Files
Z.AI-Chat-for-Android/www/js/app.js
admin 5125725ea7 v3.3.0: File Manager, SSH/Remote access, Approval gate
- File Manager: browse device files, open/preview, install APKs
- SSH/Remote: [SSH_EXEC], [SSH_UPLOAD], [SSH_DOWNLOAD], [REMOTE_EXEC], [CURL_EXEC]
- Approval gate: all sensitive actions require user approval
- New FileManagerPlugin native plugin
- Updated agentic system prompt with external access docs
- Cleaned up stale .idsig artifacts from releases/
2026-05-21 17:42:22 +04:00

3558 lines
153 KiB
JavaScript

(function() {
'use strict';
var DEFAULT_BASE_URL = 'https://api.z.ai/api/coding/paas/v4';
var DEFAULT_MODEL = 'glm-5.1';
var STORAGE_KEY = 'zai_chat_';
var MODE_PROMPTS = {
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
coding: 'You are an expert coding assistant with internal tool access. Use action tags when execution is needed: [CREATE_FILE ...][/CREATE_FILE], [RUN_COMMAND]...[/RUN_COMMAND], [BUILD_APK project], [INSTALL_APK path], [VENV_SETUP], [VENV_PIP_INSTALL package]. Keep responses concise and complete.',
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 agent with full control of this app: internal Linux, terminal, virtual env, build tools, device UI, and EXTERNAL network access.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell (any command — runs inside app sandbox):\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## External SSH — run commands on remote machines:\n[SSH_EXEC user@host command]\n[SSH_UPLOAD local_path user@host:remote_path]\n[SSH_DOWNLOAD user@host:remote_path local_path]\n\n## External network tools:\n[REMOTE_EXEC host command]\n[CURL_EXEC url]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\n\n## In-App Virtual Env:\n[VENV_SETUP]\n[VENV_PIP_INSTALL package_name]\n\n## AutoGLM Device Control:\n[DEVICE_TAP x y]\n[DEVICE_LONG_PRESS x y]\n[DEVICE_SWIPE startX startY endX endY]\n[DEVICE_TYPE text]\n[DEVICE_PRESS_BACK]\n[DEVICE_PRESS_HOME]\n[DEVICE_PRESS_RECENTS]\n[DEVICE_SCREENSHOT]\n[DEVICE_UI_TREE]\n[DEVICE_CLICK_TEXT button text]\n[DEVICE_CLICK_ID com.example:id/viewId]\n[DEVICE_LAUNCH com.example.app]\n[DEVICE_CURRENT_APP]\n\n## Hermes Agent:\n[HERMES_INSTALL]\n[HERMES_EXEC command]\n\n## Rules:\n1. Prefer internal sandbox tools first\n2. Use SSH/REMOTE actions for external machines — user must approve each one\n3. Use [CREATE_FILE], then [BUILD_APK], then [INSTALL_APK]\n4. Use [DEVICE_*] for device control; start with [DEVICE_UI_TREE]\n5. Generate complete files, never stubs\n6. For Java: package ai.z.app, target SDK 36\n7. For SSH: assume openssh-client is available, use key-based auth when possible\n8. Output [TASK_COMPLETE] only when all work is done'
};
var BUILD_SCRIPT = [
'#!/system/bin/sh',
'set -e',
'PROJECT_DIR=$(pwd)',
'BUILD_DIR="$PROJECT_DIR/build"',
'',
'AAPT2=$(command -v aapt2 2>/dev/null || echo "$TOOLS/bin/aapt2")',
'ECJ=$(command -v ecj 2>/dev/null || echo "$TOOLS/bin/ecj")',
'D8=$(command -v d8 2>/dev/null || echo "$TOOLS/bin/d8")',
'APKSIGNER=$(command -v apksigner 2>/dev/null || echo "$TOOLS/bin/apksigner")',
'',
'if [ ! -x "$AAPT2" ] && [ ! -f "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found"; exit 1; fi',
'if [ ! -f "$ECJ" ]; then echo "[BUILD FAILED] ecj not found"; exit 1; fi',
'if [ ! -f "$D8" ]; then echo "[BUILD FAILED] d8 not found"; exit 1; fi',
'',
'ANDROID_JAR=$(dirname "$AAPT2")/../share/aapt2/android.jar',
'if [ ! -f "$ANDROID_JAR" ]; then ANDROID_JAR="$TOOLS/share/android.jar"; fi',
'if [ ! -f "$ANDROID_JAR" ]; then ANDROID_JAR="$PREFIX/share/aapt2/android.jar"; fi',
'if [ ! -f "$ANDROID_JAR" ]; then echo "[BUILD FAILED] android.jar not found"; exit 1; fi',
'',
'rm -rf "$BUILD_DIR"',
'mkdir -p "$BUILD_DIR/gen" "$BUILD_DIR/classes" "$BUILD_DIR/apk"',
'',
'echo "[*] Starting build..."',
'',
'if [ -d "app/src/main/res" ] && [ "$(find app/src/main/res -type f 2>/dev/null | head -1)" ]; then',
' echo "[*] Compiling resources..."',
' "$AAPT2" compile --dir app/src/main/res -o "$BUILD_DIR/compiled_resources.zip" 2>&1 || { echo "[BUILD FAILED] Resource compilation failed"; exit 1; }',
' RES_ARG="-R $BUILD_DIR/compiled_resources.zip"',
'else',
' echo "[*] No resources, skipping..."',
' RES_ARG=""',
'fi',
'',
'echo "[*] Linking APK..."',
'"$AAPT2" link -o "$BUILD_DIR/app.unsigned.apk" \\',
' -I "$ANDROID_JAR" \\',
' --manifest app/src/main/AndroidManifest.xml \\',
' $RES_ARG \\',
' --java "$BUILD_DIR/gen" 2>&1 || { echo "[BUILD FAILED] APK linking failed"; exit 1; }',
'',
'echo "[*] Compiling Java..."',
'rm -f "$BUILD_DIR/sources.txt"',
'find app/src/main/java -name "*.java" >> "$BUILD_DIR/sources.txt" 2>/dev/null',
'find "$BUILD_DIR/gen" -name "*.java" >> "$BUILD_DIR/sources.txt" 2>/dev/null',
'if [ ! -s "$BUILD_DIR/sources.txt" ]; then echo "[BUILD FAILED] No Java source files found"; exit 1; fi',
'sh "$ECJ" -source 11 -target 11 -classpath "$ANDROID_JAR" -d "$BUILD_DIR/classes" @"$BUILD_DIR/sources.txt" 2>&1 || { echo "[BUILD FAILED] Java compilation failed"; exit 1; }',
'',
'echo "[*] Converting to DEX..."',
'find "$BUILD_DIR/classes" -name "*.class" > "$BUILD_DIR/classfiles.txt"',
'if [ ! -s "$BUILD_DIR/classfiles.txt" ]; then echo "[BUILD FAILED] No class files compiled"; exit 1; fi',
'sh "$D8" --output "$BUILD_DIR" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] D8 failed"; exit 1; }',
'if [ ! -f "$BUILD_DIR/classes.dex" ]; then echo "[BUILD FAILED] classes.dex not found"; exit 1; fi',
'',
'echo "[*] Packaging..."',
'cd "$BUILD_DIR/apk"',
'unzip -o ../app.unsigned.apk > /dev/null 2>&1',
'cp ../classes.dex .',
'rm -rf META-INF',
'zip -r ../app.unaligned.apk . > /dev/null 2>&1',
'cd "$PROJECT_DIR"',
'',
'echo "[*] Signing..."',
'KEYSTORE="$HOME/.android-debug.keystore"',
'if [ ! -f "$KEYSTORE" ]; then',
' KEYTOOL=$(command -v keytool 2>/dev/null)',
' if [ -n "$KEYTOOL" ]; then',
' "$KEYTOOL" -genkeypair -v -keystore "$KEYSTORE" -storepass android \\',
' -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 \\',
' -validity 10000 -dname "CN=Debug,O=Debug,C=US" 2>/dev/null',
' else',
' openssl genrsa -out /tmp/dbg.key 2048 2>/dev/null && \\',
' openssl req -new -key /tmp/dbg.key -out /tmp/dbg.csr -subj "/CN=Debug/O=Debug/C=US" 2>/dev/null && \\',
' openssl x509 -req -days 10000 -in /tmp/dbg.csr -signkey /tmp/dbg.key -out /tmp/dbg.crt 2>/dev/null && \\',
' openssl pkcs12 -export -in /tmp/dbg.crt -inkey /tmp/dbg.key -out "$KEYSTORE" -name androiddebugkey -password pass:android 2>/dev/null && \\',
' rm -f /tmp/dbg.key /tmp/dbg.csr /tmp/dbg.crt',
' fi',
'fi',
'',
'sh "$APKSIGNER" sign --ks "$KEYSTORE" --ks-pass pass:android --ks-key-alias androiddebugkey --key-pass pass:android "$BUILD_DIR/app.unaligned.apk" 2>&1 || true',
'mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"',
'',
'APK_PATH="$BUILD_DIR/app-signed.apk"',
'APK_SIZE=$(du -h "$APK_PATH" 2>/dev/null | cut -f1)',
'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)"'
].join('\n');
var state = {
apiKey: '',
baseUrl: DEFAULT_BASE_URL,
model: DEFAULT_MODEL,
temperature: 0.7,
maxTokens: 4096,
streaming: true,
webSearch: false,
currentMode: 'chat',
theme: 'dark',
conversations: [],
activeConversationId: null,
isGenerating: false,
abortController: null,
streamingConvId: null,
streamingContent: '',
streamingResponseDiv: null,
terminalOpen: false,
keepAwake: false,
autoDeploy: true,
autoInstallBuiltApk: true,
lastBuiltApkPath: '',
maxRetries: 10,
autoContinue: true,
maxAutoContinue: 5
};
function $(sel) { return document.querySelector(sel); }
function $$(sel) { return document.querySelectorAll(sel); }
function loadState() {
try {
state.apiKey = localStorage.getItem(STORAGE_KEY + 'apiKey') || '';
state.baseUrl = localStorage.getItem(STORAGE_KEY + 'baseUrl') || DEFAULT_BASE_URL;
state.model = localStorage.getItem(STORAGE_KEY + 'model') || DEFAULT_MODEL;
state.temperature = parseFloat(localStorage.getItem(STORAGE_KEY + 'temperature')) || 0.7;
state.maxTokens = parseInt(localStorage.getItem(STORAGE_KEY + 'maxTokens')) || 4096;
state.streaming = localStorage.getItem(STORAGE_KEY + 'streaming') !== 'false';
state.webSearch = localStorage.getItem(STORAGE_KEY + 'webSearch') === 'true';
state.currentMode = localStorage.getItem(STORAGE_KEY + 'currentMode') || 'chat';
state.theme = localStorage.getItem(STORAGE_KEY + 'theme') || 'dark';
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
state.autoInstallBuiltApk = localStorage.getItem(STORAGE_KEY + 'autoInstallBuiltApk') !== 'false';
state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10;
state.autoContinue = localStorage.getItem(STORAGE_KEY + 'autoContinue') !== 'false';
state.maxAutoContinue = parseInt(localStorage.getItem(STORAGE_KEY + 'maxAutoContinue')) || 5;
var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
state.conversations = convData ? JSON.parse(convData) : [];
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
} catch(e) { console.error('Load state error:', e); }
}
function saveState() {
try {
localStorage.setItem(STORAGE_KEY + 'apiKey', state.apiKey);
localStorage.setItem(STORAGE_KEY + 'baseUrl', state.baseUrl);
localStorage.setItem(STORAGE_KEY + 'model', state.model);
localStorage.setItem(STORAGE_KEY + 'temperature', state.temperature.toString());
localStorage.setItem(STORAGE_KEY + 'maxTokens', state.maxTokens.toString());
localStorage.setItem(STORAGE_KEY + 'streaming', state.streaming.toString());
localStorage.setItem(STORAGE_KEY + 'webSearch', state.webSearch.toString());
localStorage.setItem(STORAGE_KEY + 'currentMode', state.currentMode);
localStorage.setItem(STORAGE_KEY + 'theme', state.theme);
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.toString());
localStorage.setItem(STORAGE_KEY + 'autoInstallBuiltApk', state.autoInstallBuiltApk.toString());
localStorage.setItem(STORAGE_KEY + 'maxRetries', state.maxRetries.toString());
localStorage.setItem(STORAGE_KEY + 'autoContinue', state.autoContinue.toString());
localStorage.setItem(STORAGE_KEY + 'maxAutoContinue', state.maxAutoContinue.toString());
localStorage.setItem(STORAGE_KEY + 'conversations', JSON.stringify(state.conversations));
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
} catch(e) { console.error('Save state error:', e); }
}
function genId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 9); }
function getConversation(id) {
var targetId = id || state.activeConversationId;
if (!targetId) return null;
return state.conversations.find(function(c) { return c.id === targetId; });
}
function flushStreamingToConversation() {
if (state.streamingConvId && state.streamingContent) {
var conv = getConversation(state.streamingConvId);
if (conv) {
var lastMsg = conv.messages[conv.messages.length - 1];
if (lastMsg && lastMsg.role === 'assistant' && lastMsg._streaming) {
lastMsg.content = state.streamingContent;
delete lastMsg._streaming;
} else {
conv.messages.push({ role: 'assistant', content: state.streamingContent });
}
saveState();
}
}
state.streamingConvId = null;
state.streamingContent = '';
state.streamingResponseDiv = null;
}
function newConversation() {
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
}
var conv = {
id: genId(),
title: 'New Chat',
mode: state.currentMode,
messages: [],
files: [],
createdAt: Date.now()
};
state.conversations.unshift(conv);
state.activeConversationId = conv.id;
state.isGenerating = false;
state.abortController = null;
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateSendButton();
updateTerminalVisibility();
}
function switchConversation(id) {
if (id === state.activeConversationId) {
closeSidebar();
return;
}
flushStreamingToConversation();
if (state.isGenerating) {
stopGeneration();
state.isGenerating = false;
state.abortController = null;
}
state.activeConversationId = id;
var conv = getConversation();
if (conv) {
state.currentMode = conv.mode || 'chat';
updateModeSelector();
}
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateSendButton();
updateTerminalVisibility();
closeSidebar();
}
function deleteConversation(id) {
if (state.streamingConvId === id) {
flushStreamingToConversation();
if (state.isGenerating) stopGeneration();
state.isGenerating = false;
}
state.conversations = state.conversations.filter(function(c) { return c.id !== id; });
if (state.activeConversationId === id) {
state.activeConversationId = state.conversations.length > 0 ? state.conversations[0].id : null;
}
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateTerminalVisibility();
}
function updateHeader() {
var conv = getConversation();
$('#conversation-title').textContent = conv ? conv.title : 'Z.AI Chat';
$('#current-mode-label').textContent = state.currentMode.charAt(0).toUpperCase() + state.currentMode.slice(1);
}
function showScreen(name) {
$$('.screen').forEach(function(s) { s.classList.remove('active'); });
$('#' + name + '-screen').classList.add('active');
}
function autoResize(el) {
el.style.height = 'auto';
el.style.height = Math.min(el.scrollHeight, 120) + 'px';
}
function renderConversationList() {
var list = $('#conversation-list');
if (!list) return;
list.innerHTML = '';
state.conversations.forEach(function(conv) {
var div = document.createElement('div');
div.className = 'conv-item' + (conv.id === state.activeConversationId ? ' active' : '');
var msgCount = conv.messages.length;
div.innerHTML = '<span class="conv-title">' + escapeHtml(conv.title) +
(msgCount > 0 ? ' <span style="color:var(--text-muted);font-size:11px">(' + msgCount + ')</span>' : '') +
'</span>' +
'<button class="conv-delete" data-id="' + conv.id + '">&times;</button>';
div.addEventListener('click', function(e) {
if (e.target.classList.contains('conv-delete')) {
e.stopPropagation();
deleteConversation(e.target.dataset.id);
return;
}
switchConversation(conv.id);
});
list.appendChild(div);
});
}
function escapeHtml(text) {
var d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function escapeAttr(text) {
return (text || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderMarkdown(text) {
if (typeof marked !== 'undefined') {
marked.setOptions({
highlight: function(code, lang) {
if (typeof hljs !== 'undefined' && lang && hljs.getLanguage(lang)) {
try { return hljs.highlight(code, { language: lang }).value; } catch(e) {}
}
return code;
},
breaks: true,
gfm: true
});
return marked.parse(text);
}
return text.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>');
}
var EXT_MAP = {
python: 'py', py: 'py', javascript: 'js', js: 'js', typescript: 'ts', ts: 'ts',
java: 'java', kotlin: 'kt', kt: 'kt', html: 'html', css: 'css', json: 'json',
xml: 'xml', yaml: 'yml', yml: 'yml', markdown: 'md', md: 'md', sql: 'sql',
shell: 'sh', bash: 'sh', sh: 'sh', powershell: 'ps1', dockerfile: 'Dockerfile',
ruby: 'rb', go: 'go', rust: 'rs', c: 'c', cpp: 'cpp', csharp: 'cs',
swift: 'swift', php: 'php', perl: 'pl', scala: 'scala', groovy: 'groovy',
gradle: 'gradle', properties: 'properties', toml: 'toml', ini: 'ini',
dart: 'dart', lua: 'lua', r: 'r', protobuf: 'proto'
};
function guessFileName(code, lang) {
var firstLine = code.trim().split('\n')[0];
if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\|[a-zA-Z0-9_\-]+\.[a-zA-Z]{1,10})/.test(firstLine) &&
firstLine.length < 120 && firstLine.split('\n').length === 1 &&
/\.\w+$/.test(firstLine)) {
return firstLine.replace(/^.*[\/\\]/, '');
}
var ext = EXT_MAP[lang] || lang || 'txt';
return 'code.' + ext;
}
function downloadFile(content, filename) {
var blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
}
function addCodeHeaders(container) {
container.querySelectorAll('pre code').forEach(function(block) {
var pre = block.parentElement;
var lang = (block.className.match(/language-(\w+)/) || [])[1] || 'code';
if (!pre.previousElementSibling || !pre.previousElementSibling.classList.contains('code-header')) {
var header = document.createElement('div');
header.className = 'code-header';
var fileName = guessFileName(block.textContent, lang);
header.innerHTML = '<span>' + escapeHtml(lang) + '</span>' +
'<div class="code-header-actions">' +
'<button class="download-btn" data-filename="' + escapeHtml(fileName) + '">Save</button>' +
'<button class="copy-btn">Copy</button></div>';
pre.parentElement.insertBefore(header, pre);
header.querySelector('.copy-btn').addEventListener('click', function() {
navigator.clipboard.writeText(block.textContent).then(function() {
this.textContent = 'Copied!';
setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000);
}.bind(this));
});
header.querySelector('.download-btn').addEventListener('click', function() {
var fn = this.getAttribute('data-filename');
var code = block.textContent;
if (fn && /\.\w+$/.test(fn)) {
var lines = code.split('\n');
if (lines[0].trim() === fn || lines[0].trim().endsWith('/' + fn)) {
code = lines.slice(1).join('\n');
}
}
downloadFile(code, fn);
this.textContent = 'Saved!';
setTimeout(function() { this.textContent = 'Save'; }.bind(this), 2000);
});
}
});
}
function highlightFilePaths(html) {
return html.replace(
/(^|[\s(>])(\/(?:[\w\-\.]+\/){1,}[\w\-\.]+\.\w{1,10})([\s)<,]|$)/gm,
'$1<span class="filepath-badge" tabindex="0">$2</span>$3'
);
}
function addFilePathHandlers(container) {
container.querySelectorAll('.filepath-badge').forEach(function(badge) {
badge.addEventListener('click', function() {
var path = this.textContent;
navigator.clipboard.writeText(path).then(function() {
badge.style.background = 'var(--success)';
badge.style.borderColor = 'var(--success)';
badge.style.color = 'white';
setTimeout(function() {
badge.style.background = '';
badge.style.borderColor = '';
badge.style.color = '';
}, 1500);
});
});
});
}
function renderMessages() {
var container = $('#messages');
if (!container) return;
container.innerHTML = '';
var conv = getConversation();
if (!conv || conv.messages.length === 0) {
container.innerHTML = '<div class="message system">Start a conversation with Z.AI</div>';
return;
}
conv.messages.forEach(function(msg) {
appendMessage(msg.role, msg.content, container, false);
});
container.scrollTop = container.scrollHeight;
updateTerminalContent();
}
function appendMessage(role, content, container, animate) {
container = container || $('#messages');
var div = document.createElement('div');
div.className = 'message ' + role;
if (animate === false) div.style.animation = 'none';
if (role === 'assistant') {
div.innerHTML = renderMarkdown(content);
addCodeHeaders(div);
var processed = highlightFilePaths(div.innerHTML);
if (processed !== div.innerHTML) {
div.innerHTML = processed;
addFilePathHandlers(div);
}
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
actionBar.innerHTML =
'<button class="msg-action-btn msg-copy-btn">Copy</button>' +
'<button class="msg-action-btn msg-save-btn">Save .txt</button>';
div.appendChild(actionBar);
actionBar.querySelector('.msg-copy-btn').addEventListener('click', function() {
var btn = this;
navigator.clipboard.writeText(content).then(function() {
btn.textContent = 'Copied!';
btn.classList.add('copied');
setTimeout(function() { btn.textContent = 'Copy'; btn.classList.remove('copied'); }, 2000);
});
});
actionBar.querySelector('.msg-save-btn').addEventListener('click', function() {
var btn = this;
var conv = getConversation();
var fn = (conv ? conv.title : 'response').replace(/[^a-zA-Z0-9_\-]/g, '_').substring(0, 40);
downloadFile(content, fn + '.txt');
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;
}
container.appendChild(div);
container.scrollTop = container.scrollHeight;
return div;
}
function updateStreamingMessage(div, content) {
div.innerHTML = renderMarkdown(content);
addCodeHeaders(div);
var processed = highlightFilePaths(div.innerHTML);
if (processed !== div.innerHTML) {
div.innerHTML = processed;
addFilePathHandlers(div);
}
var actions = div.querySelector('.msg-actions');
if (!actions) {
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
actionBar.innerHTML =
'<button class="msg-action-btn msg-copy-btn">Copy</button>' +
'<button class="msg-action-btn msg-save-btn">Save .txt</button>';
div.appendChild(actionBar);
}
$('#messages').scrollTop = $('#messages').scrollHeight;
}
function showThinking() {
var container = $('#messages');
var div = document.createElement('div');
div.className = 'message assistant';
div.id = 'thinking-msg';
div.innerHTML = '<div class="thinking-indicator"><div class="thinking-dots"><span></span><span></span><span></span></div> Thinking...</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
function removeThinking() {
var el = $('#thinking-msg');
if (el) el.remove();
}
async function sendMessage() {
var input = $('#message-input');
var text = input.value.trim();
if (!text || state.isGenerating) return;
if (!state.apiKey) {
showScreen('setup');
return;
}
if ((state.currentMode === 'coding' || state.currentMode === 'agentic') && !termState.devToolsInstalled) {
checkDevEnvironment();
}
if (!state.activeConversationId) {
newConversation();
}
var conv = getConversation();
if (!conv) return;
conv.mode = state.currentMode;
if (conv.messages.length === 0) {
conv.title = text.substring(0, 50) + (text.length > 50 ? '...' : '');
updateHeader();
renderConversationList();
}
conv.messages.push({ role: 'user', content: text });
saveState();
input.value = '';
autoResize(input);
updateSendButton();
appendMessage('user', text);
state.isGenerating = true;
state.streamingConvId = conv.id;
state.streamingContent = '';
updateSendButton();
showThinking();
if (state.keepAwake) setWakeLock(true);
var requestBody = null;
var responseDiv = null;
try {
var systemPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat;
if (Shell && (state.currentMode === 'agentic' || state.currentMode === 'coding')) {
var wsCtx = await getWorkspaceContext();
if (wsCtx) systemPrompt += '\n\n## Current Device Context:\n' + wsCtx;
}
var apiMessages = [{ role: 'system', content: systemPrompt }];
conv.messages.forEach(function(m) {
if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) {
apiMessages.push({ role: m.role, content: m.content });
}
});
requestBody = {
model: state.model,
messages: apiMessages,
temperature: state.temperature,
max_tokens: state.maxTokens,
stream: state.streaming
};
if (state.webSearch) {
requestBody.tools = [{
type: 'web_search',
web_search: { search_query: text, search_result: true }
}];
}
removeThinking();
responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
if (state.streaming) {
await streamResponseWithRetry(requestBody, responseDiv, conv);
} else {
var result = await apiRequestWithRetry(requestBody);
var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content);
state.streamingContent = content;
conv.messages.push({ role: 'assistant', content: content });
}
} catch(err) {
removeThinking();
if (err.name !== 'AbortError') {
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
appendRetryMessage(err, requestBody, conv);
} else if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
} finally {
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
saveState();
updateTerminalContent();
if (state.autoDeploy && (state.currentMode === 'coding' || state.currentMode === 'agentic')) {
var finalContent = state.streamingContent || '';
if (!finalContent && conv && conv.messages.length) {
var last = conv.messages[conv.messages.length - 1];
if (last && last.role === 'assistant') finalContent = last.content;
}
if (finalContent) {
var autoActions = parseAiActions(finalContent);
if (autoActions.length > 0) {
trackFilesFromActions(autoActions, conv);
await autoExecuteActions(autoActions, conv);
}
}
renderFileTree();
}
if (state.autoContinue && (state.currentMode === 'coding' || state.currentMode === 'agentic')) {
await autoContinueIfNeeded(conv);
}
state.isGenerating = false;
updateSendButton();
if (state.keepAwake) setWakeLock(false);
}
}
function isNetworkError(err) {
if (!err) return false;
var msg = (err.message || '').toLowerCase();
var name = (err.name || '').toLowerCase();
return name === 'typeerror' || name === 'networkerror' ||
msg.indexOf('failed to fetch') >= 0 ||
msg.indexOf('network') >= 0 ||
msg.indexOf('load failed') >= 0 ||
msg.indexOf('connection') >= 0 ||
msg.indexOf('net::') >= 0 ||
msg.indexOf('interrupted') >= 0;
}
function sleep(ms) {
return new Promise(function(r) { setTimeout(r, ms); });
}
async function apiRequestWithRetry(body, maxRetries) {
maxRetries = maxRetries || 3;
var lastErr;
for (var attempt = 0; attempt < maxRetries; attempt++) {
try {
return await apiRequest(body);
} catch(err) {
lastErr = err;
if (!isNetworkError(err) || attempt >= maxRetries - 1) throw err;
var delay = 1000 * Math.pow(2, attempt);
appendMessage('system', 'Connection lost. Retrying in ' + (delay / 1000) + 's... (attempt ' + (attempt + 2) + '/' + maxRetries + ')');
await sleep(delay);
}
}
throw lastErr;
}
var _streamAutoSaveCounter = 0;
async function streamResponseWithRetry(body, responseDiv, conv, maxRetries) {
maxRetries = maxRetries || 3;
var lastErr;
for (var attempt = 0; attempt < maxRetries; attempt++) {
try {
await streamResponse(body, responseDiv, conv, attempt > 0);
return;
} catch(err) {
lastErr = err;
if (err.name === 'AbortError') throw err;
if (!isNetworkError(err)) throw err;
if (attempt >= maxRetries - 1) throw err;
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: true });
saveState();
}
var delay = 1500 * Math.pow(2, attempt);
var retryNotice = document.createElement('div');
retryNotice.className = 'message system';
retryNotice.innerHTML = '<div style="display:flex;align-items:center;gap:6px;justify-content:center">' +
'<div class="thinking-dots"><span></span><span></span><span></span></div>' +
' Reconnecting... (attempt ' + (attempt + 2) + '/' + maxRetries + ')</div>';
$('#messages').appendChild(retryNotice);
$('#messages').scrollTop = $('#messages').scrollHeight;
await sleep(delay);
if (retryNotice.parentElement) retryNotice.remove();
var lastAssistant = '';
for (var mi = conv.messages.length - 1; mi >= 0; mi--) {
if (conv.messages[mi].role === 'assistant' && !conv.messages[mi]._streaming) {
lastAssistant = conv.messages[mi].content;
break;
}
}
body.messages = body.messages.filter(function(m) { return m.role !== 'assistant'; });
if (state.streamingContent) {
body.messages.push({ role: 'assistant', content: state.streamingContent });
}
body.stream = true;
if (responseDiv && responseDiv.parentElement) {
var currentText = state.streamingContent || lastAssistant;
updateStreamingMessage(responseDiv, currentText + '\n\n*--- connection interrupted, resuming ---*\n');
state.streamingContent = currentText;
}
}
}
throw lastErr;
}
async function streamResponse(body, responseDiv, conv, isRetry) {
state.abortController = new AbortController();
body.stream = true;
var url = state.baseUrl.replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + state.apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify(body),
signal: state.abortController.signal
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'API error ' + resp.status);
}
var reader = resp.body.getReader();
var decoder = new TextDecoder();
var fullContent = '';
var buffer = '';
_streamAutoSaveCounter = 0;
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var lines = buffer.split('\n');
buffer = lines.pop() || '';
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (!line || !line.startsWith('data:')) continue;
var data = line.substring(5).trim();
if (data === '[DONE]') break;
try {
var parsed = JSON.parse(data);
var delta = parsed.choices && parsed.choices[0] && parsed.choices[0].delta;
if (delta && delta.content) {
fullContent += delta.content;
state.streamingContent = fullContent;
if (responseDiv && responseDiv.parentElement) {
updateStreamingMessage(responseDiv, fullContent);
}
_streamAutoSaveCounter++;
if (_streamAutoSaveCounter % 20 === 0) {
_saveStreamingProgress(conv, fullContent);
}
}
} catch(e) {}
}
}
conv.messages.push({ role: 'assistant', content: fullContent });
state.streamingContent = '';
}
function _saveStreamingProgress(conv, content) {
if (!conv) return;
var last = conv.messages[conv.messages.length - 1];
if (last && last.role === 'assistant' && last._streaming) {
last.content = content;
} else {
conv.messages.push({ role: 'assistant', content: content, _streaming: true });
}
saveState();
}
function stopGeneration() {
if (state.abortController) {
state.abortController.abort();
}
flushStreamingToConversation();
}
async function apiRequest(body) {
var url = state.baseUrl.replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + state.apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify(body)
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'API error ' + resp.status);
}
return await resp.json();
}
function appendRetryMessage(err, requestBody, conv) {
var container = $('#messages');
var div = document.createElement('div');
div.className = 'message system';
var isNet = isNetworkError(err);
div.innerHTML = '<div style="text-align:center">' +
'<div style="color:var(--danger);margin-bottom:8px">' +
(isNet ? 'Connection interrupted' : escapeHtml(err.message || 'Request failed')) +
'</div>' +
(isNet ? '<button class="retry-btn" id="retry-btn">Retry</button>' : '') +
'</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
if (isNet) {
setTimeout(function() {
var btn = $('#retry-btn');
if (btn) btn.addEventListener('click', function() {
if (div.parentElement) div.remove();
retryLastRequest(requestBody, conv);
});
}, 50);
}
}
async function retryLastRequest(requestBody, conv) {
if (!requestBody || !conv) return;
state.isGenerating = true;
state.streamingConvId = conv.id;
updateSendButton();
var responseDiv = appendMessage('assistant', '');
state.streamingResponseDiv = responseDiv;
try {
if (state.streaming) {
requestBody.stream = true;
await streamResponseWithRetry(requestBody, responseDiv, conv);
} else {
var result = await apiRequestWithRetry(requestBody);
var content = result.choices[0].message.content;
updateStreamingMessage(responseDiv, content);
state.streamingContent = content;
conv.messages.push({ role: 'assistant', content: content });
}
} catch(err) {
removeThinking();
if (state.streamingContent) {
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
appendRetryMessage(err, requestBody, conv);
} finally {
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
updateTerminalContent();
}
}
function setupVisibilityHandler() {
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'hidden') {
flushStreamingToConversation();
}
});
window.addEventListener('online', function() {
var msg = $('#offline-msg');
if (msg) msg.remove();
});
window.addEventListener('offline', function() {
var container = $('#messages');
if (container && !$('#offline-msg')) {
var div = document.createElement('div');
div.className = 'message system';
div.id = 'offline-msg';
div.innerHTML = '<div style="color:var(--warning)">You are offline. Messages will be saved and sent when connection is restored.</div>';
container.appendChild(div);
container.scrollTop = container.scrollHeight;
}
});
}
function updateSendButton() {
var input = $('#message-input');
var sendBtn = $('#send-btn');
var stopBtn = $('#stop-btn');
if (state.isGenerating) {
sendBtn.style.display = 'none';
stopBtn.style.display = 'flex';
} else {
sendBtn.style.display = 'flex';
stopBtn.style.display = 'none';
sendBtn.disabled = !input.value.trim();
}
}
function updateModeSelector() {
$$('.mode-btn').forEach(function(btn) {
btn.classList.toggle('active', btn.dataset.mode === state.currentMode);
});
}
function openSidebar() {
$('#sidebar').classList.add('open');
$('#sidebar-overlay').classList.add('active');
}
function closeSidebar() {
$('#sidebar').classList.remove('open');
$('#sidebar-overlay').classList.remove('active');
}
function applyTheme(theme) {
state.theme = theme;
document.documentElement.setAttribute('data-theme', theme);
var headerBtn = $('#theme-toggle-header');
if (headerBtn) {
headerBtn.innerHTML = theme === 'dark' ? '&#9788;' : '&#9790;';
headerBtn.title = theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode';
}
var settingsToggle = $('#settings-darkmode');
if (settingsToggle) settingsToggle.checked = (theme === 'dark');
var metaTheme = document.querySelector('meta[name="theme-color"]');
if (metaTheme) metaTheme.content = theme === 'dark' ? '#1a1a2e' : '#ffffff';
saveState();
}
function toggleTheme() {
applyTheme(state.theme === 'dark' ? 'light' : 'dark');
}
// ---- Terminal Panel ----
function parseTerminalEntries(content) {
if (!content) return [];
var entries = [];
var toolRegex = /\[(CREATE_FILE|EDIT_FILE|DELETE_FILE|RUN_COMMAND|SEARCH|READ_FILE|BUILD|TEST)\]\s*\(([^)]*)\)\s*\n?([\s\S]*?)(?=\n\[|$)/gi;
var match;
while ((match = toolRegex.exec(content)) !== null) {
entries.push({
type: 'tool',
action: match[1],
target: match[2].trim(),
body: match[3].trim()
});
}
var codeBlockRegex = /```(\w*)\n([\s\S]*?)```/gi;
var idx = 0;
while ((match = codeBlockRegex.exec(content)) !== null) {
var isTool = false;
for (var e = 0; e < entries.length; e++) {
if (entries[e].type === 'tool' && match.index >= content.indexOf(entries[e].body) - 20) {
isTool = true;
break;
}
}
if (!isTool) {
idx++;
var firstLine = match[2].trim().split('\n')[0];
var isFilePath = /^(\/|\.\/|\.\.\/|[A-Za-z]:\\|[a-zA-Z0-9_\-]+\.[a-zA-Z]{1,4}$)/.test(firstLine) && firstLine.length < 120 && firstLine.split('\n').length === 1;
entries.push({
type: 'code',
lang: match[1] || 'text',
code: match[2].trim(),
index: idx,
fileName: isFilePath ? firstLine : null
});
}
}
entries.sort(function(a, b) {
return content.indexOf(a.type === 'tool' ? '[' + a.action : '```' + (a.lang || '')) -
content.indexOf(b.type === 'tool' ? '[' + b.action : '```' + (b.lang || ''));
});
return entries;
}
function renderTerminalEntry(entry) {
if (entry.type === 'tool') {
var actionIcon = { CREATE_FILE: '+', EDIT_FILE: '~', DELETE_FILE: '-', RUN_COMMAND: '>', SEARCH: '?', READ_FILE: 'R', BUILD: 'B', TEST: 'T' };
var actionColor = { CREATE_FILE: 'var(--success)', EDIT_FILE: 'var(--warning)', DELETE_FILE: 'var(--danger)', RUN_COMMAND: 'var(--accent)', SEARCH: 'var(--text-secondary)', READ_FILE: 'var(--text-muted)', BUILD: 'var(--accent)', TEST: 'var(--success)' };
var icon = actionIcon[entry.action] || '>';
var color = actionColor[entry.action] || 'var(--accent)';
var html = '<div class="term-entry term-tool" style="border-left:3px solid ' + color + '">';
html += '<div class="term-tool-header"><span class="term-action" style="color:' + color + '">[' + icon + '] ' + escapeHtml(entry.action) + '</span>';
if (entry.target) html += ' <span class="term-target">' + escapeHtml(entry.target) + '</span>';
html += '</div>';
if (entry.body) {
html += '<pre class="term-code">' + escapeHtml(entry.body.substring(0, 2000)) + (entry.body.length > 2000 ? '\n... (truncated)' : '') + '</pre>';
}
html += '</div>';
return html;
}
if (entry.type === 'code') {
var label = entry.fileName ? escapeHtml(entry.fileName) : escapeHtml(entry.lang || 'code');
var displayCode = entry.fileName ? entry.code.split('\n').slice(1).join('\n') : entry.code;
if (!displayCode.trim()) displayCode = entry.code;
var html = '<div class="term-entry term-code-block">';
html += '<div class="term-file-header"><span class="term-lang">' + label + '</span>';
html += '<button class="term-copy-btn" data-code="' + escapeHtml(entry.code).replace(/"/g, '&quot;') + '">Copy</button></div>';
html += '<pre class="term-code">' + escapeHtml(displayCode.substring(0, 3000)) + (displayCode.length > 3000 ? '\n... (truncated)' : '') + '</pre>';
html += '</div>';
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 = '<div class="term-empty">No code output yet. Use Coding or Agentic mode to generate code.</div>';
return;
}
var content = state.streamingContent || (lastAssistant ? lastAssistant.content : '');
var entries = parseTerminalEntries(content);
if (entries.length === 0) {
termBody.innerHTML = '<div class="term-empty">No structured code blocks or tool calls detected in response.</div>';
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(/&quot;/g, '"').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/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 = '&#9654; 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 = '&#128230; 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 = '&#128241; 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 += '<div class="ftree-folder">';
html += '<div class="ftree-node ftree-dir" style="padding-left:' + (indent + 8) + 'px">';
html += '<span class="ftree-arrow">&#9654;</span>';
html += '<span class="ftree-dirname">' + escapeHtml(dirs[d]) + '</span>';
html += '</div>';
html += '<div class="ftree-children">';
html += renderTreeHtml(child, convId, depth + 1);
html += '</div></div>';
}
for (var fi = 0; fi < node.files.length; fi++) {
var file = node.files[fi];
html += '<div class="ftree-node ftree-file" style="padding-left:' + (indent + 20) + 'px" ';
html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">';
html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>';
html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>';
if (isApkPath(file.path)) {
html += '<span class="ftree-badge">APK</span>';
}
html += '</div>';
}
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 = '<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>';
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 = '<div class="ftree-empty">File manager unavailable.</div>';
return;
}
var items = result.items || [];
if (items.length === 0) {
body.innerHTML = '<div class="ftree-empty">Folder is empty.</div>';
return;
}
var html = '<div class="device-files-root">';
for (var i = 0; i < items.length; i++) {
var item = items[i];
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
html += '<span class="ftree-ext">' + escapeHtml(item.directory ? 'dir' : (item.mimeType || '?')) + '</span>';
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
html += item.directory ? '<span class="ftree-badge">DIR</span>' : (isApkPath(item.path) ? '<span class="ftree-badge">APK</span>' : '');
html += '</div>';
}
html += '</div>';
body.innerHTML = html;
}
async function renderFileManagerHome() {
var body = $('#file-tree-body');
if (!body) return;
if (!FileManager) {
body.innerHTML = '<div class="ftree-empty">File manager unavailable.</div>';
return;
}
try {
var result = await FileManager.getRoots();
var roots = result.roots || [];
var html = '<div class="device-files-root">';
for (var i = 0; i < roots.length; i++) {
var item = roots[i];
html += '<div class="ftree-node ftree-file device-file-row" data-path="' + escapeAttr(item.path) + '">';
html += '<span class="ftree-ext">root</span>';
html += '<span class="ftree-fname">' + escapeHtml(item.name) + '</span>';
html += '<span class="ftree-badge">OPEN</span>';
html += '</div>';
}
html += '</div>';
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 = '<div class="ftree-empty">File manager unavailable.</div>';
}
}
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 = '<span style="color:var(--success)">Device control enabled</span>';
if (btn) { btn.textContent = 'Device Control Active'; btn.disabled = true; }
} else {
statusEl.innerHTML = '<span style="color:var(--warning)">Not enabled — tap to open Settings</span>';
}
} catch(e) {
statusEl.innerHTML = '<span style="color:var(--text-secondary)">Checking status...</span>';
}
}
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 = '<span class="dtb-icon">&#9888;</span> ' +
'<span class="dtb-msg">' + msg + '</span>' +
'<button class="dtb-install-btn">Install</button>' +
'<button class="dtb-dismiss-btn">&times;</button>';
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 = '<p style="color:var(--danger)">Shell test failed: ' + (shellTest.output || 'unknown error') + '</p>' +
'<p>Try restarting the app.</p>';
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Retry Install';
btn.querySelector('.btn-loader').style.display = 'none';
return;
}
statusEl.innerHTML = '<p style="color:var(--success);font-size:16px;font-weight:700">&#10004; Termux environment installed!</p>' +
'<p id="devsetup-tools-status">Installing build tools (aapt2, ecj, d8, apksigner)...</p>';
var toolsOk = await ensureBuildTools();
var toolsStatus = $('#devsetup-tools-status');
if (toolsStatus) {
toolsStatus.innerHTML = toolsOk
? '<span style="color:var(--success)">&#10004; All tools installed — ready to build!</span>'
: '<span style="color:var(--warning)">Build tools not installed.<br>1. Install Termux from F-Droid<br>2. Run: pkg install aapt2 ecj dx apksigner<br>3. Z.AI Chat will auto-detect Termux tools.</span>';
}
btn.querySelector('.btn-text').textContent = 'Installed';
btn.querySelector('.btn-loader').style.display = 'none';
} catch(e) {
statusEl.innerHTML = '<p style="color:var(--danger)">Install failed: ' + e.message + '</p>' +
'<p>Check your internet connection and try again.</p>';
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Retry Install';
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');
});
}
var autoglmBtn = $('#autoglm-enable-btn');
if (autoglmBtn) {
checkAutoGLMStatus();
autoglmBtn.addEventListener('click', function() {
var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM;
if (AutoGLM) {
AutoGLM.openSettings();
}
});
}
var hermesBtn = $('#hermes-install-btn');
if (hermesBtn) {
hermesBtn.addEventListener('click', async function() {
var Bootstrap = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Bootstrap;
if (!Bootstrap) return;
hermesBtn.disabled = true;
hermesBtn.textContent = 'Installing Hermes...';
try {
var result = await Bootstrap.installHermes({});
hermesBtn.textContent = 'Hermes Installed!';
hermesBtn.disabled = true;
termState.hermesPath = result.path;
termState.hermesVenv = result.venv;
termPrint('[OK] Hermes installed: ' + result.path, 'success');
} catch(e) {
hermesBtn.textContent = 'Install Failed - Retry';
hermesBtn.disabled = false;
termPrint('[!] Hermes install failed: ' + e.message, 'err');
}
});
}
$$('.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 v3.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) {
var url = (baseUrl || state.baseUrl).replace(/\/+$/, '') + '/chat/completions';
var resp = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiKey,
'Accept-Language': 'en-US,en'
},
body: JSON.stringify({
model: state.model,
messages: [{ role: 'user', content: 'Hi' }],
max_tokens: 10
})
});
if (!resp.ok) {
var errData = {};
try { errData = await resp.json(); } catch(e) {}
throw new Error(errData.error?.message || 'Connection failed (' + resp.status + ')');
}
return await resp.json();
}
function populateSettings() {
$('#settings-token').value = state.apiKey;
$('#settings-url').value = state.baseUrl;
$('#settings-model').value = state.model;
$('#settings-temp').value = state.temperature;
$('#temp-value').textContent = state.temperature;
$('#settings-tokens').value = state.maxTokens;
$('#tokens-value').textContent = state.maxTokens;
$('#settings-websearch').checked = state.webSearch;
$('#settings-streaming').checked = state.streaming;
$('#settings-autodeploy').checked = state.autoDeploy;
$('#settings-keepawake').checked = state.keepAwake;
$('#settings-maxretries').value = state.maxRetries;
$('#retries-value').textContent = state.maxRetries;
$('#settings-autocontinue').checked = state.autoContinue;
$('#settings-maxautocontinue').value = state.maxAutoContinue;
$('#autocont-value').textContent = state.maxAutoContinue;
}
function saveSettings() {
state.apiKey = $('#settings-token').value.trim();
state.baseUrl = $('#settings-url').value.trim();
state.model = $('#settings-model').value;
state.temperature = parseFloat($('#settings-temp').value);
state.maxTokens = parseInt($('#settings-tokens').value);
state.webSearch = $('#settings-websearch').checked;
state.streaming = $('#settings-streaming').checked;
saveState();
}
function exportConversations() {
var data = JSON.stringify(state.conversations, null, 2);
var blob = new Blob([data], { type: 'application/json' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = 'zai-chat-export-' + new Date().toISOString().slice(0, 10) + '.json';
a.click();
URL.revokeObjectURL(url);
}
function init() {
loadState();
initShellPlugins();
if (state.apiKey) {
showScreen('chat');
if (state.activeConversationId) {
var conv = getConversation();
if (conv) {
state.currentMode = conv.mode || 'chat';
}
}
renderConversationList();
renderMessages();
updateHeader();
updateModeSelector();
updateTerminalVisibility();
$('#api-token').value = state.apiKey;
$('#base-url').value = state.baseUrl;
}
$('#connect-btn').addEventListener('click', async function() {
var btn = this;
var apiKey = $('#api-token').value.trim();
var baseUrl = $('#base-url').value;
var errorEl = $('#setup-error');
if (!apiKey) {
errorEl.textContent = 'Please enter your API key';
errorEl.style.display = 'block';
return;
}
btn.disabled = true;
btn.querySelector('.btn-text').textContent = 'Connecting...';
btn.querySelector('.btn-loader').style.display = 'inline-block';
errorEl.style.display = 'none';
try {
await testConnection(apiKey, baseUrl);
state.apiKey = apiKey;
state.baseUrl = baseUrl;
saveState();
showScreen('chat');
newConversation();
} catch(err) {
errorEl.textContent = err.message;
errorEl.style.display = 'block';
} finally {
btn.disabled = false;
btn.querySelector('.btn-text').textContent = 'Connect';
btn.querySelector('.btn-loader').style.display = 'none';
}
});
$('#api-token').addEventListener('keydown', function(e) {
if (e.key === 'Enter') $('#connect-btn').click();
});
$('#message-input').addEventListener('input', function() {
autoResize(this);
updateSendButton();
});
$('#message-input').addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
$('#send-btn').addEventListener('click', sendMessage);
$('#stop-btn').addEventListener('click', stopGeneration);
$$('.mode-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var mode = this.dataset.mode;
if (mode === 'terminal') {
showScreen('terminal');
return;
}
state.currentMode = mode;
updateModeSelector();
updateHeader();
updateTerminalVisibility();
checkDevEnvironment();
saveState();
});
});
$('#menu-btn').addEventListener('click', openSidebar);
$('#sidebar-close').addEventListener('click', closeSidebar);
$('#sidebar-overlay').addEventListener('click', closeSidebar);
$('#new-chat-btn').addEventListener('click', function() { newConversation(); });
$('#new-chat-sidebar').addEventListener('click', function() { newConversation(); closeSidebar(); });
$('#settings-btn').addEventListener('click', function() {
populateSettings();
showScreen('settings');
});
$('#settings-back').addEventListener('click', function() {
saveSettings();
showScreen('chat');
});
$('#settings-temp').addEventListener('input', function() {
$('#temp-value').textContent = this.value;
});
$('#settings-tokens').addEventListener('input', function() {
$('#tokens-value').textContent = this.value;
});
$('#settings-token').addEventListener('change', saveSettings);
$('#settings-url').addEventListener('change', saveSettings);
$('#settings-model').addEventListener('change', saveSettings);
$('#settings-websearch').addEventListener('change', saveSettings);
$('#settings-streaming').addEventListener('change', saveSettings);
$('#settings-autodeploy').addEventListener('change', function() {
state.autoDeploy = this.checked;
saveState();
});
$('#settings-keepawake').addEventListener('change', function() {
state.keepAwake = this.checked;
saveState();
});
$('#settings-maxretries').addEventListener('input', function() {
$('#retries-value').textContent = this.value;
});
$('#settings-maxretries').addEventListener('change', function() {
state.maxRetries = parseInt(this.value) || 10;
saveState();
});
$('#settings-autocontinue').addEventListener('change', function() {
state.autoContinue = this.checked;
saveState();
});
$('#settings-maxautocontinue').addEventListener('input', function() {
$('#autocont-value').textContent = this.value;
});
$('#settings-maxautocontinue').addEventListener('change', function() {
state.maxAutoContinue = parseInt(this.value) || 5;
saveState();
});
$('#file-tree-btn').addEventListener('click', toggleDeviceFileManager);
$('#device-files-btn').addEventListener('click', function() {
var panel = $('#file-tree-panel');
if (panel && panel.classList.contains('open')) {
showProjectFiles();
} else {
toggleDeviceFileManager();
}
});
$('#file-tree-close').addEventListener('click', closeFileTree);
$('#file-tree-overlay').addEventListener('click', closeFileTree);
$('#file-tree-body').addEventListener('click', function(e) {
var node = e.target.closest('.ftree-file');
if (node) {
if (node.dataset.conv) {
openFileViewer(node.dataset.conv, node.dataset.path);
} else {
var title = document.querySelector('#file-tree-panel h3');
if (title && title.textContent === 'File Manager') {
renderDeviceFiles(node.dataset.path);
} else {
openFileFromTree(node.dataset.path);
}
}
} else {
node = e.target.closest('.ftree-dir');
if (node) {
var folder = node.parentElement;
folder.classList.toggle('open');
var arrow = node.querySelector('.ftree-arrow');
if (arrow) arrow.innerHTML = folder.classList.contains('open') ? '&#9660;' : '&#9654;';
}
}
});
$('#file-viewer-close').addEventListener('click', closeFileViewer);
$('#file-viewer-edit').addEventListener('click', toggleFileEdit);
$('#file-viewer-save').addEventListener('click', saveFileEdit);
$('#approval-allow').addEventListener('click', function() {
if (approvalState && approvalState.onApprove) approvalState.onApprove();
});
$('#approval-deny').addEventListener('click', closeApproval);
$('#theme-toggle-header').addEventListener('click', toggleTheme);
$('#settings-darkmode').addEventListener('change', function() {
applyTheme(this.checked ? 'dark' : 'light');
});
$('#terminal-toggle').addEventListener('click', toggleTerminal);
$('#export-btn').addEventListener('click', exportConversations);
$('#clear-btn').addEventListener('click', function() {
if (confirm('Clear all conversations? This cannot be undone.')) {
state.conversations = [];
state.activeConversationId = null;
saveState();
renderConversationList();
renderMessages();
updateHeader();
updateTerminalContent();
}
});
updateModeSelector();
updateSendButton();
applyTheme(state.theme);
setupVisibilityHandler();
initTerminal();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();