(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 coding agent with FULL control of an Android device. You have a real terminal with shell access, can read/write any file, build APKs, install apps, and execute any command.\n\n## Your Tools (use these EXACT formats):\n\n### Write a file:\n[CREATE_FILE path/to/file.ext]\nfile contents here\n[/CREATE_FILE]\n\n### Run a shell command:\n[RUN_COMMAND]\ncommand here\n[/RUN_COMMAND]\n\n### Build Android APK:\n[BUILD_APK project_name]\n\n### Install APK on device:\n[INSTALL_APK /path/to/file.apk]\n\n### List files:\n[RUN_COMMAND]\nfind . -type f | head -50\n[/RUN_COMMAND]\n\n## IMPORTANT RULES:\n1. ALWAYS use [CREATE_FILE] for EVERY file — the system auto-saves them to the device\n2. ALWAYS use [BUILD_APK] after writing files — the system auto-compiles\n3. ALWAYS use [INSTALL_APK] after building — the system auto-installs\n4. NEVER say "I installed it" unless you used [INSTALL_APK] — the system executes your tags automatically\n5. If a build fails, you will see the error output — FIX the code and try again\n6. Generate COMPLETE files — never use "// ... existing code ..."\n7. For Java: use package ai.z.app, target SDK 36, compile SDK 36\n8. You can run ANY shell command: ls, cat, mkdir, chmod, cp, grep, find, etc.\n9. If Termux tools are available, use: aapt2, d8, ecj, javac, apksigner\n10. Write ALL files first, THEN build, THEN install. Always in that order.\n11. When the user asks to build an app, generate EVERY file needed for a complete working app.\n12. CRITICAL: When you have FULLY completed the user\'s entire task (all files written, all builds done, all installs done), output [TASK_COMPLETE] on a line by itself. This is MANDATORY — the system uses it to know you are done.\n13. If your response is cut off or you haven\'t finished all work, do NOT output [TASK_COMPLETE]. The system will automatically continue you.\n14. NEVER output [TASK_COMPLETE] unless the ENTIRE task is truly done. If there are more files to write, commands to run, or builds to perform, keep working.'
};
var BUILD_SCRIPT = [
'#!/bin/sh',
'set -e',
'PROJECT_DIR=$(pwd)',
'BUILD_DIR="$PROJECT_DIR/build"',
'',
'AAPT2=$(command -v aapt2 2>/dev/null)',
'ECJ=$(command -v ecj 2>/dev/null)',
'D8=$(command -v d8 2>/dev/null)',
'DX=$(command -v dx 2>/dev/null)',
'APKSIGNER=$(command -v apksigner 2>/dev/null)',
'',
'if [ -z "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found. Install: pkg install aapt2"; exit 1; fi',
'if [ -z "$ECJ" ]; then echo "[BUILD FAILED] ecj not found. Install: pkg install ecj"; exit 1; fi',
'if [ -z "$D8" ] && [ -z "$DX" ]; then echo "[BUILD FAILED] d8/dx not found. Install: pkg install d8"; exit 1; fi',
'',
'ANDROID_JAR=$(dirname "$AAPT2")/../share/aapt2/android.jar',
'if [ ! -f "$ANDROID_JAR" ]; then echo "[BUILD FAILED] android.jar not found at $ANDROID_JAR"; 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',
'"$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',
'if [ -n "$D8" ]; then',
' "$D8" --output "$BUILD_DIR" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] D8 failed"; exit 1; }',
'else',
' "$DX" --output "$BUILD_DIR/classes.dex" @"$BUILD_DIR/classfiles.txt" 2>&1 || { echo "[BUILD FAILED] DX failed"; exit 1; }',
'fi',
'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',
'',
'if [ -f "$KEYSTORE" ] && [ -n "$APKSIGNER" ]; then',
' "$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"',
'elif [ -n "$APKSIGNER" ]; then',
' mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"',
' echo "[!] Warning: APK unsigned - no keystore"',
'else',
' mv "$BUILD_DIR/app.unaligned.apk" "$BUILD_DIR/app-signed.apk"',
' echo "[!] Warning: APK unsigned - no apksigner"',
'fi',
'',
'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 = '';
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.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 += '';
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 += '';
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,
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 codeBlockFileRegex = /```(\w+)\s*\n([\s\S]*?)```/gi;
var match;
while ((match = createActionRegex.exec(content)) !== null) {
actions.push({ type: 'create_file', path: match[1].trim(), content: match[2] });
}
while ((match = runCmdRegex.exec(content)) !== null) {
actions.push({ type: 'run_command', command: match[1].trim() });
}
while ((match = buildApkRegex.exec(content)) !== null) {
actions.push({ type: 'build_apk', project: match[1].trim() });
}
while ((match = installApkRegex.exec(content)) !== null) {
actions.push({ type: 'install_apk', path: match[1].trim() });
}
while ((match = codeBlockFileRegex.exec(content)) !== null) {
var lang = match[1];
var code = match[2];
var firstLine = code.trim().split('\n')[0];
if (/^(\/|\.\/|\.\.\/|[A-Za-z]:\\)/.test(firstLine) && firstLine.length < 120 && /\.\w+$/.test(firstLine.split('\n')[0])) {
var filePath = firstLine.trim();
var fileContent = code.trim().split('\n').slice(1).join('\n');
actions.push({ type: 'create_file', path: filePath, content: fileContent });
}
}
return actions;
}
function addActionButtons(div, actions) {
if (actions.length === 0) return;
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
var hasCommands = actions.some(function(a) { return a.type === 'run_command'; });
var hasBuild = actions.some(function(a) { return a.type === 'build_apk'; });
var hasInstall = actions.some(function(a) { return a.type === 'install_apk'; });
var actionBar = document.createElement('div');
actionBar.className = 'msg-actions';
if (hasFiles) {
var deployBtn = document.createElement('button');
deployBtn.className = 'deploy-btn';
deployBtn.innerHTML = '▶ Deploy Files';
deployBtn.addEventListener('click', function() { deployActions(actions); });
actionBar.appendChild(deployBtn);
}
if (hasBuild) {
var buildBtn = document.createElement('button');
buildBtn.className = 'deploy-btn';
buildBtn.style.background = 'linear-gradient(135deg, var(--accent), #a855f7)';
buildBtn.innerHTML = '📦 Build APK';
buildBtn.addEventListener('click', function() { buildFromActions(actions); });
actionBar.appendChild(buildBtn);
}
if (hasInstall) {
actions.forEach(function(action) {
if (action.type === 'install_apk') {
var installBtn = document.createElement('button');
installBtn.className = 'install-apk-btn';
installBtn.innerHTML = '📱 Install APK';
installBtn.addEventListener('click', function() { installApk(action.path); });
actionBar.appendChild(installBtn);
}
});
}
div.appendChild(actionBar);
}
async function deployActions(actions) {
showScreen('terminal');
termPrint('\n--- Deploying files ---', 'info');
for (var i = 0; i < actions.length; i++) {
var action = actions[i];
if (action.type === 'create_file') {
var path = action.path;
if (!path.startsWith('/')) {
path = (termState.projectsDir || termState.homeDir + '/projects') + '/' + path;
}
var dir = path.substring(0, path.lastIndexOf('/'));
await shellMkdirs(dir);
var ok = await shellWriteFile(path, action.content);
if (ok) {
termPrint(' [+] ' + path + ' (' + action.content.length + ' bytes)', 'success');
} else {
termPrint(' [!] Failed: ' + path, 'err');
}
}
}
termPrint('--- Deploy complete ---\n', 'info');
}
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;
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 && command -v ecj && (command -v d8 || command -v dx)', termState.homeDir, false);
if (check.exitCode === 0) return true;
termPrint('[*] Installing build tools...', 'info');
showStatusToast('Installing build tools (first time)...', 'info');
var installResult = await shellExec(
'apt update -y 2>&1 && apt install -y aapt2 ecj dx apksigner 2>&1 || pkg install -y aapt2 ecj dx apksigner 2>&1',
termState.homeDir, false
);
if (installResult.output) {
termPrint(installResult.output.replace(/\n$/, ''), '');
}
var recheck = await shellExec('command -v aapt2 && command -v ecj', termState.homeDir, false);
if (recheck.exitCode === 0) {
termPrint('[OK] Build tools installed', 'success');
showStatusToast('Build tools installed!', 'success');
return true;
}
termPrint('[!] Failed to install build tools automatically', 'err');
termPrint('Please run manually: pkg install aapt2 ecj dx apksigner', 'warning');
return false;
}
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'; });
if (!hasFiles && !hasBuild && !hasInstall && !hasCommands) 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 (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 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();
statusEl.innerHTML = '✔ Termux environment installed!
' +
'Full Linux shell with bash, coreutils, and package manager ready.
' +
'Installing build tools (aapt2, ecj, d8, apksigner)...
';
btn.querySelector('.btn-text').textContent = 'Installed';
btn.querySelector('.btn-loader').style.display = 'none';
termState.homeDir = result.prefixDir ? result.prefixDir.replace('/usr', '') : termState.homeDir;
termState.cwd = termState.homeDir + '/home';
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 toolsOk = await ensureBuildTools();
var toolsStatus = $('#devsetup-tools-status');
if (toolsStatus) {
toolsStatus.innerHTML = toolsOk
? '✔ Build tools installed (aapt2, ecj, d8, apksigner)'
: 'Build tools not installed. Run: pkg install aapt2 ecj dx apksigner';
}
} 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');
});
}
$$('.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();
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();
}
})();