(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. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.', brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.', agentic: 'You are an autonomous agent with FULL control of an Android device. You have a real terminal, can build APKs, control the device UI via AutoGLM, and use Hermes agent tools.\n\n## File Operations:\n[CREATE_FILE path/to/file.ext]\ncontents\n[/CREATE_FILE]\n\n## Shell:\n[RUN_COMMAND]\ncommand\n[/RUN_COMMAND]\n\n## Build:\n[BUILD_APK project_name]\n[INSTALL_APK /path/to/file.apk]\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. Use [CREATE_FILE] for files, [BUILD_APK] to compile, [INSTALL_APK] to install\n2. Use [DEVICE_*] for device control — first [DEVICE_UI_TREE] to see screen, then interact\n3. Use [HERMES_EXEC] for Hermes capabilities — web search, terminal, skills, memory\n4. Generate COMPLETE files, never stubs\n5. For Java: package ai.z.app, target SDK 36\n6. Output [TASK_COMPLETE] ONLY when ALL work is done\n7. Never say done unless all files written, builds done, installs 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, 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.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 + '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 = '' + escapeHtml(conv.title) + (msgCount > 0 ? ' (' + msgCount + ')' : '') + '' + ''; 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, '&').replace(/"/g, '"').replace(/'/g, ''').replace(//g, '>'); } 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, '>').replace(/\n/g, '
'); } 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 = '' + escapeHtml(lang) + '' + '
' + '' + '
'; 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$2$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 = '
Start a conversation with Z.AI
'; 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 = '' + ''; 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 = '' + ''; 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 = '
Thinking...
'; 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 }); } var retryDiv = 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 = '
' + '
' + ' Reconnecting... (attempt ' + (attempt + 2) + '/' + maxRetries + ')
'; $('#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 = '
' + '
' + (isNet ? 'Connection interrupted' : escapeHtml(err.message || 'Request failed')) + '
' + (isNet ? '' : '') + '
'; 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 = '
You are offline. Messages will be saved and sent when connection is restored.
'; 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' ? '☼' : '☾'; 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 = '
'; html += '
[' + icon + '] ' + escapeHtml(entry.action) + ''; if (entry.target) html += ' ' + escapeHtml(entry.target) + ''; html += '
'; if (entry.body) { html += '
' + escapeHtml(entry.body.substring(0, 2000)) + (entry.body.length > 2000 ? '\n... (truncated)' : '') + '
'; } html += '
'; 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 = '
'; html += '
' + label + ''; html += '
'; html += '
' + escapeHtml(displayCode.substring(0, 3000)) + (displayCode.length > 3000 ? '\n... (truncated)' : '') + '
'; html += '
'; return html; } return ''; } function updateTerminalContent() { var termBody = $('#terminal-body'); if (!termBody) return; termBody.innerHTML = ''; var conv = getConversation(); if (!conv) return; var lastAssistant = null; for (var i = conv.messages.length - 1; i >= 0; i--) { if (conv.messages[i].role === 'assistant') { lastAssistant = conv.messages[i]; break; } } if (!lastAssistant && !state.streamingContent) { termBody.innerHTML = '
No code output yet. Use Coding or Agentic mode to generate code.
'; return; } var content = state.streamingContent || (lastAssistant ? lastAssistant.content : ''); var entries = parseTerminalEntries(content); if (entries.length === 0) { termBody.innerHTML = '
No structured code blocks or tool calls detected in response.
'; return; } entries.forEach(function(entry) { termBody.innerHTML += renderTerminalEntry(entry); }); termBody.querySelectorAll('.term-copy-btn').forEach(function(btn) { btn.addEventListener('click', function() { var code = this.getAttribute('data-code').replace(/"/g, '"').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); navigator.clipboard.writeText(code).then(function() { this.textContent = 'Copied!'; setTimeout(function() { this.textContent = 'Copy'; }.bind(this), 2000); }.bind(this)); }); }); if (state.terminalOpen) { termBody.scrollTop = termBody.scrollHeight; } } function updateTerminalVisibility() { var panel = $('#terminal-panel'); var toggleBtn = $('#terminal-toggle'); var fileTreeBtn = $('#file-tree-btn'); if (!panel || !toggleBtn) return; var isDevMode = (state.currentMode === 'coding' || state.currentMode === 'agentic'); if (isDevMode) { panel.style.display = 'flex'; toggleBtn.style.display = 'flex'; if (fileTreeBtn) fileTreeBtn.style.display = ''; } else { panel.style.display = 'none'; toggleBtn.style.display = 'none'; if (fileTreeBtn) fileTreeBtn.style.display = 'none'; state.terminalOpen = false; closeFileTree(); } if (state.terminalOpen && isDevMode) { panel.classList.add('open'); } else { panel.classList.remove('open'); } var label = toggleBtn.querySelector('.terminal-label'); if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal'; } function toggleTerminal() { state.terminalOpen = !state.terminalOpen; var panel = $('#terminal-panel'); if (panel) panel.classList.toggle('open', state.terminalOpen); var label = $('#terminal-toggle .terminal-label'); if (label) label.textContent = state.terminalOpen ? 'Hide Terminal' : 'Show Terminal'; if (state.terminalOpen) updateTerminalContent(); saveState(); } // ---- Terminal & Shell System ---- var Shell = null; var Installer = null; var Wake = null; var Bootstrap = null; var termState = { history: [], historyIndex: -1, cwd: null, homeDir: null, toolsDir: null, projectsDir: null, isRunning: false, activePid: null, activeStreamId: null, devToolsInstalled: false, javaToolsInstalled: false, hasProot: false, prootPath: '', nativeLibDir: '', hermesPath: '', hermesVenv: '', autoglmEnabled: false, commandQueue: [] }; function initShellPlugins() { try { Shell = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Shell; Installer = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Installer; 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 (!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 }; } } async function shellWriteFile(path, content) { if (!Shell) return false; try { await Shell.writeFile({ path: path, content: content }); return true; } catch(e) { return false; } } async function shellReadFile(path) { if (!Shell) return null; try { var result = await Shell.readFile({ path: path }); return result.content; } catch(e) { return null; } } async function shellMkdirs(path) { if (!Shell) return false; try { await Shell.mkdirs({ path: path }); return true; } catch(e) { return false; } } async function installApk(path) { if (!Installer) { termPrint('[Installer plugin not available]', 'err'); return; } try { var result = await Installer.installApk({ path: path }); termPrint('[APK install triggered: ' + path + ']', 'success'); } catch(e) { termPrint('[Install failed: ' + e.message + ']', 'err'); } } async function getDeviceInfo() { if (!Installer) return {}; try { return await Installer.getDeviceInfo(); } catch(e) { return {}; } } 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; 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 hermesInstallRegex = /\[HERMES_INSTALL\]/gi; var hermesExecRegex = /\[HERMES_EXEC\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 = codeBlockFileRegex.exec(content)) !== null) { var lang = match[1]; var code = match[2]; var firstLine = code.trim().split('\n')[0]; if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\)/.test(firstLine) && firstLine.length < 120 && /\.\w+$/.test(firstLine.split('\n')[0])) { var filePath = firstLine.trim(); var fileContent = code.trim().split('\n').slice(1).join('\n'); actions.push({ type: 'create_file', path: filePath, content: fileContent }); } } return actions; } function addActionButtons(div, actions) { if (actions.length === 0) return; var hasFiles = actions.some(function(a) { return a.type === 'create_file'; }); var hasCommands = actions.some(function(a) { return a.type === 'run_command'; }); var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; }); var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; }); var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; }); var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; }); var actionBar = document.createElement('div'); actionBar.className = 'msg-actions'; if (hasFiles) { var deployBtn = document.createElement('button'); deployBtn.className = 'deploy-btn'; deployBtn.innerHTML = '▶ Deploy Files'; deployBtn.addEventListener('click', function() { deployActions(actions); }); actionBar.appendChild(deployBtn); } if (hasBuild) { var buildBtn = document.createElement('button'); buildBtn.className = 'deploy-btn'; buildBtn.style.background = 'linear-gradient(135deg, var(--accent), #a855f7)'; buildBtn.innerHTML = '📦 Build APK'; buildBtn.addEventListener('click', function() { buildFromActions(actions); }); actionBar.appendChild(buildBtn); } if (hasInstall) { actions.forEach(function(action) { if (action.type === 'install_apk') { var installBtn = document.createElement('button'); installBtn.className = 'install-apk-btn'; installBtn.innerHTML = '📱 Install APK'; installBtn.addEventListener('click', function() { installApk(action.path); }); actionBar.appendChild(installBtn); } }); } div.appendChild(actionBar); } async function deployActions(actions) { showScreen('terminal'); termPrint('\n--- Deploying files ---', 'info'); for (var i = 0; i < actions.length; i++) { var action = actions[i]; if (action.type === 'create_file') { var path = action.path; if (!path.startsWith('/')) { path = (termState.projectsDir || termState.homeDir + '/projects') + '/' + path; } var dir = path.substring(0, path.lastIndexOf('/')); await shellMkdirs(dir); var ok = await shellWriteFile(path, action.content); if (ok) { termPrint(' [+] ' + path + ' (' + action.content.length + ' bytes)', 'success'); } else { termPrint(' [!] Failed: ' + path, 'err'); } } } termPrint('--- Deploy complete ---\n', 'info'); } function showStatusToast(message, type) { var existing = $('#status-toast'); if (existing) existing.remove(); var toast = document.createElement('div'); toast.id = 'status-toast'; toast.style.cssText = 'position:fixed;bottom:80px;left:50%;transform:translateX(-50%);z-index:1000;' + 'padding:10px 20px;border-radius:20px;font-size:13px;font-weight:600;max-width:90%;' + 'text-align:center;pointer-events:none;opacity:0;transition:opacity 0.3s;' + 'background:' + (type === 'success' ? 'var(--success)' : type === 'err' ? 'var(--danger)' : 'var(--accent)') + ';color:white;box-shadow:0 4px 12px rgba(0,0,0,0.3)'; toast.textContent = message; document.body.appendChild(toast); requestAnimationFrame(function() { toast.style.opacity = '1'; }); setTimeout(function() { toast.style.opacity = '0'; setTimeout(function() { if (toast.parentElement) toast.remove(); }, 300); }, 3000); } var _agenticRetryCount = 0; function getMaxRetries() { return state.maxRetries || 10; } function getMaxAutoContinue() { return state.maxAutoContinue || 5; } function getFileLanguage(path) { var ext = (path || '').split('.').pop().toLowerCase(); var map = { 'java': 'java', 'xml': 'xml', 'html': 'html', 'css': 'css', 'js': 'javascript', 'json': 'json', 'md': 'markdown', 'py': 'python', 'kt': 'kotlin', 'gradle': 'groovy', 'properties': 'properties', 'txt': 'text', 'sh': 'shell', 'yaml': 'yaml', 'yml': 'yaml', 'toml': 'toml', 'c': 'c', 'cpp': 'cpp', 'h': 'c', 'rs': 'rust', 'sql': 'sql', 'svg': 'svg', 'png': 'image', 'jpg': 'image' }; return map[ext] || ext; } function trackFile(conv, path, content) { if (!conv || !path) return; if (!conv.files) conv.files = []; var lang = getFileLanguage(path); var idx = -1; for (var i = 0; i < conv.files.length; i++) { if (conv.files[i].path === path) { idx = i; break; } } var entry = { path: path, content: content, language: lang, timestamp: Date.now() }; if (idx >= 0) { conv.files[idx] = entry; } else { conv.files.push(entry); } saveState(); } function trackFilesFromActions(actions, conv) { if (!conv) return; for (var i = 0; i < actions.length; i++) { if (actions[i].type === 'create_file') { trackFile(conv, actions[i].path, actions[i].content); } } } function resolveFilePath(path) { if (!path) return ''; if (path.startsWith('/')) return path; return (termState.projectsDir || termState.homeDir + '/projects') + '/' + path; } function getLastAssistantContent(conv) { if (!conv || !conv.messages) return ''; for (var i = conv.messages.length - 1; i >= 0; i--) { if (conv.messages[i].role === 'assistant' && !conv.messages[i]._streaming) { return conv.messages[i].content; } } return ''; } function isTaskComplete(content) { if (!content) return true; if (content.indexOf('[TASK_COMPLETE]') >= 0) return true; var hasAction = content.indexOf('[CREATE_FILE') >= 0 || content.indexOf('[RUN_COMMAND]') >= 0 || content.indexOf('[BUILD_APK') >= 0 || content.indexOf('[INSTALL_APK') >= 0 || content.indexOf('[DEVICE_') >= 0 || content.indexOf('[HERMES_') >= 0; var hasCodeBlock = content.indexOf('```') >= 0; if (!hasCodeBlock && !hasAction && content.length < 300) return true; if ((content.match(/```/g) || []).length % 2 !== 0) return false; if ((content.match(/\[CREATE_FILE/g) || []).length > (content.match(/\[\/CREATE_FILE\]/g) || []).length) return false; if ((content.match(/\[RUN_COMMAND\]/g) || []).length > (content.match(/\[\/RUN_COMMAND\]/g) || []).length) return false; if (hasAction) return false; return true; } async function autoContinueIfNeeded(conv) { if (!state.autoContinue) return; if (state.currentMode !== 'coding' && state.currentMode !== 'agentic') return; if (!conv || !state.apiKey) return; var maxCont = getMaxAutoContinue(); var count = 0; while (count < maxCont) { var last = getLastAssistantContent(conv); if (isTaskComplete(last)) break; count++; termPrint('\n[>] Auto-continuing (' + count + '/' + maxCont + ')...', 'info'); showStatusToast('Auto-continuing (' + count + '/' + maxCont + ')...', 'info'); var contMsg = 'Your previous response was cut off or the task is not fully complete. ' + 'Continue from EXACTLY where you left off. Complete ALL remaining files, commands, builds, and installations. ' + 'When the ENTIRE task is fully done, output [TASK_COMPLETE] on its own line.'; conv.messages.push({ role: 'user', content: contMsg }); saveState(); appendMessage('user', contMsg); state.isGenerating = true; state.streamingConvId = conv.id; state.streamingContent = ''; updateSendButton(); if (state.keepAwake) setWakeLock(true); var contError = false; try { var sysPrompt = MODE_PROMPTS[state.currentMode] || MODE_PROMPTS.chat; if (Shell && (state.currentMode === 'agentic' || state.currentMode === 'coding')) { var wsCtx = await getWorkspaceContext(); if (wsCtx) sysPrompt += '\n\n## Current Device Context:\n' + wsCtx; } var apiMsgs = [{ role: 'system', content: sysPrompt }]; conv.messages.forEach(function(m) { if (m.role === 'user' || (m.role === 'assistant' && !m._streaming)) { apiMsgs.push({ role: m.role, content: m.content }); } }); var reqBody = { model: state.model, messages: apiMsgs, temperature: state.temperature, max_tokens: state.maxTokens, stream: state.streaming }; var respDiv = appendMessage('assistant', ''); state.streamingResponseDiv = respDiv; if (state.streaming) { await streamResponseWithRetry(reqBody, respDiv, conv); } else { var res = await apiRequestWithRetry(reqBody); var cont = res.choices[0].message.content; updateStreamingMessage(respDiv, cont); state.streamingContent = cont; conv.messages.push({ role: 'assistant', content: cont }); } } catch(err) { if (state.streamingContent) { conv.messages.push({ role: 'assistant', content: state.streamingContent }); } if (err.name !== 'AbortError') { termPrint('[!] Auto-continue error: ' + (err.message || err), 'err'); } contError = true; } state.isGenerating = false; state.abortController = null; state.streamingConvId = null; state.streamingResponseDiv = null; updateSendButton(); saveState(); if (state.keepAwake) setWakeLock(false); if (contError) break; var contContent = state.streamingContent || ''; state.streamingContent = ''; if (state.autoDeploy && contContent) { var contActions = parseAiActions(contContent); if (contActions.length > 0) { trackFilesFromActions(contActions, conv); await autoExecuteActions(contActions, conv); } } renderFileTree(); } if (count > 0) { var finalCheck = getLastAssistantContent(conv); if (isTaskComplete(finalCheck)) { termPrint('\n[v] Task completed after ' + count + ' auto-continue(s)', 'success'); showStatusToast('Task completed!', 'success'); } else if (count >= maxCont) { termPrint('\n[!] Max auto-continues reached (' + maxCont + ')', 'warning'); } } } function buildFileTreeData(files) { var root = { name: '', children: {}, files: [] }; for (var f = 0; f < files.length; f++) { var parts = files[f].path.split('/').filter(Boolean); var cur = root; for (var i = 0; i < parts.length - 1; i++) { if (!cur.children[parts[i]]) { cur.children[parts[i]] = { name: parts[i], children: {}, files: [] }; } cur = cur.children[parts[i]]; } cur.files.push({ name: parts[parts.length - 1], path: files[f].path, language: files[f].language, timestamp: files[f].timestamp }); } return root; } function renderTreeHtml(node, convId, depth) { var html = ''; var indent = depth * 20; var dirs = Object.keys(node.children).sort(); for (var d = 0; d < dirs.length; d++) { var child = node.children[dirs[d]]; html += '
'; html += '
'; html += ''; html += '' + escapeHtml(dirs[d]) + ''; html += '
'; html += '
'; html += renderTreeHtml(child, convId, depth + 1); html += '
'; } for (var fi = 0; fi < node.files.length; fi++) { var file = node.files[fi]; html += '
'; html += '' + escapeHtml(file.language || '?') + ''; html += '' + escapeHtml(file.name) + ''; html += '
'; } return html; } function renderFileTree() { var body = $('#file-tree-body'); if (!body) return; var conv = getConversation(); if (!conv || !conv.files || conv.files.length === 0) { body.innerHTML = '
No files yet.
AI-generated files appear here.
'; var badge = $('#file-tree-count'); if (badge) badge.textContent = '0 files'; return; } var tree = buildFileTreeData(conv.files); body.innerHTML = renderTreeHtml(tree, conv.id, 0); var badge = $('#file-tree-count'); if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : ''); } 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 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) 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; } 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 pkgCmd = 'sh /usr/bin/pkg update -y 2>&1 && sh /usr/bin/pkg install -y aapt2 ecj dx apksigner 2>&1'; if (!new File(pkgBin).exists) 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 { var runResult = await Bootstrap.runInTermux({command: 'pkg update -y && pkg install -y aapt2 ecj dx apksigner'}); termPrint('[*] Command sent to Termux. Waiting...', 'info'); await new Promise(function(r) { setTimeout(r, 15000); }); if (await toolsReady()) return true; await new Promise(function(r) { setTimeout(r, 30000); }); if (await toolsReady()) return true; } catch(e) { termPrint('[!] Termux RUN_COMMAND failed: ' + e.message, 'err'); } return false; } async function checkAutoGLMStatus() { var AutoGLM = window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.AutoGLM; var statusEl = $('#autoglm-status'); var btn = $('#autoglm-enable-btn'); if (!AutoGLM || !statusEl) return; try { var result = await AutoGLM.isEnabled(); if (result.enabled) { statusEl.innerHTML = 'Device control enabled'; if (btn) { btn.textContent = 'Device Control Active'; btn.disabled = true; } } else { statusEl.innerHTML = 'Not enabled — tap to open Settings'; } } catch(e) { statusEl.innerHTML = 'Checking status...'; } } async function checkDevEnvironment() { if (state.currentMode !== 'coding' && state.currentMode !== 'agentic') return; if (!Bootstrap) return; try { var bsStatus = await Bootstrap.getStatus(); if (!bsStatus.installed) { showDevToolsBanner('Termux not installed. Tap Dev Setup to install Linux environment + build tools.'); return; } } catch(e) {} if (termState.devToolsInstalled) return; var termuxCheck = await shellExec('command -v aapt2 >/dev/null 2>&1 && command -v ecj >/dev/null 2>&1', termState.homeDir, false); if (termuxCheck.exitCode === 0) { termState.devToolsInstalled = true; return; } if (termState.toolsDir) { var javaCheck = await shellExec('test -f "' + termState.toolsDir + '/jars/ecj.jar" && test -f "' + termState.toolsDir + '/jars/d8.jar"', termState.homeDir, false); if (javaCheck.exitCode === 0) { termState.devToolsInstalled = true; termState.javaToolsInstalled = true; return; } } showDevToolsBanner('Build tools not installed. Tap to auto-install via Java virtual environment.'); } function showDevToolsBanner(msg) { var existing = $('#dev-tools-banner'); if (existing) existing.remove(); var container = $('#messages'); if (!container) return; var banner = document.createElement('div'); banner.id = 'dev-tools-banner'; banner.className = 'dev-tools-banner'; banner.innerHTML = ' ' + '' + msg + '' + '' + ''; container.insertBefore(banner, container.firstChild); banner.querySelector('.dtb-install-btn').addEventListener('click', async function() { var btn = this; btn.textContent = 'Installing...'; btn.disabled = true; var bsStatus; try { bsStatus = await Bootstrap.getStatus(); } catch(e) { bsStatus = { installed: false }; } if (!bsStatus.installed) { try { await Bootstrap.install(); try { await Bootstrap.fixPermissions(); } catch(e) {} if (Shell) { var env = await Shell.getEnv(); termState.homeDir = env.HOME; termState.toolsDir = env.TOOLS; termState.projectsDir = env.PROJECTS; termState.cwd = env.CWD || env.HOME; } updateCwdDisplay(); await shellExec('echo shell-ok', termState.homeDir, false); } catch(e) { btn.textContent = 'Bootstrap failed: ' + e.message; return; } } var ok = await ensureBuildTools(); if (ok) { banner.remove(); showStatusToast('All tools installed!', 'success'); } else { btn.textContent = 'Retry'; btn.disabled = false; } }); banner.querySelector('.dtb-dismiss-btn').addEventListener('click', function() { banner.remove(); }); } async function autoExecuteActions(actions, conv) { var hasFiles = actions.some(function(a) { return a.type === 'create_file'; }); var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; }); var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; }); var hasCommands = actions.some(function(a) { return a.type === 'run_command'; }); var hasDevice = actions.some(function(a) { return a.type && a.type.indexOf('device_') === 0; }); var hasHermes = actions.some(function(a) { return a.type && a.type.indexOf('hermes_') === 0; }); if (!hasFiles && !hasBuild && !hasInstall && !hasCommands && !hasDevice && !hasHermes) 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') { showStatusToast('Running: ' + actions[c].command.substring(0, 40), 'info'); var cmdResult = await shellExec(actions[c].command, termState.cwd, false); resultLog.push('CMD: ' + actions[c].command.substring(0, 60) + '\nexit: ' + (cmdResult.exitCode !== undefined ? cmdResult.exitCode : '?') + '\n' + (cmdResult.output || '').substring(0, 500)); if (cmdResult.output) { termPrint('$ ' + actions[c].command, 'cmd'); termPrint(cmdResult.output.replace(/\n$/, ''), cmdResult.exitCode === 0 ? '' : 'err'); } } } } 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 (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 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)' : ''; termPrint('\n[VERIFIED] APK built successfully: ' + apkPath + sizeInfo, 'success'); return '[BUILD OK] ' + apkPath; } 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 projectDir = termState.projectsDir || (termState.homeDir + '/projects'); for (var i = 0; i < actions.length; i++) { var action = actions[i]; if (action.type === 'create_file') { var path = action.path; if (!path.startsWith('/')) path = projectDir + '/' + path; var dir = path.substring(0, path.lastIndexOf('/')); await shellMkdirs(dir); await shellWriteFile(path, action.content); termPrint(' [+] ' + path, 'success'); } } termPrint('\nBuilding with aapt2 + d8...', 'info'); var buildCmd = 'cd ' + projectDir + ' && ' + 'if [ -d "app/src/main" ]; then ' + ' AAPT2=$(which aapt2 2>/dev/null || echo "") && ' + ' D8=$(which d8 2>/dev/null || echo "") && ' + ' ECJ=$(which ecj 2>/dev/null || echo "") && ' + ' if [ -z "$AAPT2" ]; then echo "[!] aapt2 not found. Run Setup Dev Tools first."; exit 1; fi && ' + ' echo "[*] Compiling resources..." && ' + ' $AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' + ' echo "[*] Linking..." && ' + ' $AAPT2 link -o build/app.unsigned.apk ' + ' -I tools/android.jar ' + ' --manifest app/src/main/AndroidManifest.xml ' + ' -R build/compiled_resources.zip ' + ' --java build/gen 2>&1 && ' + ' echo "[*] Compiling Java..." && ' + ' find app/src/main/java -name "*.java" > build/sources.txt 2>/dev/null && ' + ' $ECJ -source 11 -target 11 -classpath tools/android.jar -d build/classes @build/sources.txt 2>&1 && ' + ' echo "[*] Converting to DEX..." && ' + ' $D8 --output build/ build/classes/**/*.class 2>&1 && ' + ' echo "[*] Packaging..." && ' + ' cd build && cp app.unsigned.apk app.unaligned.apk && ' + ' mkdir -p app.unaligned.apk.tmp && cd app.unaligned.apk.tmp && ' + ' unzip -o ../app.unaligned.apk && ' + ' cp ../classes.dex . && ' + ' zip -r ../app.unaligned.apk . && cd .. && rm -rf app.unaligned.apk.tmp && ' + ' echo "[*] Signing..." && ' + ' java -jar tools/uber-apk-signer.jar -a app.unaligned.apk --overwrite 2>&1 || ' + ' cp app.unaligned.apk app-signed.apk && ' + ' echo "[OK] APK built: ' + projectDir + '/build/app-signed.apk" && ' + ' echo "Size: $(du -h app-signed.apk | cut -f1)" ; ' + 'else echo "[!] No app/src/main found. Deploy files first."; fi'; await termExec(buildCmd); } // ---- Dev Tools Setup ---- var DEV_TOOLS = [ { name: 'bash', url: 'https://github.com/termux/termux-packages/releases/download/bash-v5.2.21/bash-v5.2.21-aarch64.zip', type: 'binary' }, { name: 'coreutils', url: 'https://github.com/termux/termux-packages/releases/download/coreutils-9.4/coreutils-9.4-aarch64.zip', type: 'binary' } ]; async function checkDevTools() { if (!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 { var result = 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 = '

Shell test failed: ' + (shellTest.output || 'unknown error') + '

' + '

Try restarting the app.

'; btn.disabled = false; btn.querySelector('.btn-text').textContent = 'Retry Install'; btn.querySelector('.btn-loader').style.display = 'none'; return; } statusEl.innerHTML = '

✔ Termux environment installed!

' + '

Installing build tools (aapt2, ecj, d8, apksigner)...

'; var toolsOk = await ensureBuildTools(); var toolsStatus = $('#devsetup-tools-status'); if (toolsStatus) { toolsStatus.innerHTML = toolsOk ? '✔ All tools installed — ready to build!' : 'Build tools not installed.
1. Install Termux from F-Droid
2. Run: pkg install aapt2 ecj dx apksigner
3. Z.AI Chat will auto-detect Termux tools.
'; } btn.querySelector('.btn-text').textContent = 'Installed'; btn.querySelector('.btn-loader').style.display = 'none'; } catch(e) { statusEl.innerHTML = '

Install failed: ' + e.message + '

' + '

Check your internet connection and try again.

'; 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 v1.3.0', 'info'); termPrint('Home: ' + termState.homeDir, 'info'); termPrint('Type "help" for commands, "setup" for dev tools\n', 'info'); }).catch(function() {}); } } // ---- Terminal command handler ---- var origTermExec = termExec; termExec = async function(command) { if (!command.trim()) return; var lower = command.trim().toLowerCase(); if (lower === 'help') { termPrint('$ help', 'cmd'); termPrint('Z.AI Terminal Commands:', 'info'); termPrint(' help - Show this help', ''); termPrint(' setup - Open dev tools setup', ''); termPrint(' sysinfo - Show device info', ''); termPrint(' create NAME - Create new Android project', ''); termPrint(' install APK - Install an APK file', ''); termPrint(' clear - Clear terminal', ''); termPrint(' exit - Back to chat', ''); termPrint('', ''); termPrint('Shell: Any standard Linux command works here.', ''); termPrint('Tip: Use "setup" to install build tools (aapt2, d8, ecj)\n', ''); return; } if (lower === 'setup') { showScreen('devsetup'); return; } if (lower === 'sysinfo') { termPrint('$ sysinfo', 'cmd'); var info = await getDeviceInfo(); termPrint('Device: ' + (info.manufacturer || '') + ' ' + (info.model || ''), ''); termPrint('Android: ' + (info.release || '?') + ' (SDK ' + (info.sdk || '?') + ')', ''); termPrint('ABI: ' + (info.abi || '?'), ''); termPrint('Files: ' + (info.filesDir || '?'), ''); termPrint('Package: ' + (info.package || '?') + '\n', ''); return; } if (lower.startsWith('create ')) { var name = command.trim().substring(7).trim(); termPrint('$ create ' + name, 'cmd'); var projectDir = termState.projectsDir || (termState.homeDir + '/projects'); await shellExec('sh ' + termState.toolsDir + '/setup.sh', termState.homeDir, false); await shellExec('sh ' + termState.toolsDir + '/create-project.sh ' + name, termState.homeDir, false); return; } if (lower.startsWith('install ')) { var path = command.trim().substring(8).trim(); termPrint('$ install ' + path, 'cmd'); await installApk(path); return; } if (lower === 'clear') { var output = $('#term-output'); if (output) output.innerHTML = ''; return; } if (lower === 'exit') { showScreen('chat'); return; } await origTermExec(command); }; // ---- Rest of init ---- async function testConnection(apiKey, baseUrl) { 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', toggleFileTree); $('#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) { openFileViewer(node.dataset.conv, 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') ? '▼' : '▶'; } } }); $('#file-viewer-close').addEventListener('click', closeFileViewer); $('#file-viewer-edit').addEventListener('click', toggleFileEdit); $('#file-viewer-save').addEventListener('click', saveFileEdit); $('#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(); } })();