v2.1.0: File tree per session, auto-continue for incomplete tasks, task completion protocol

This commit is contained in:
admin
2026-05-19 18:39:14 +04:00
Unverified
parent 5572f2cb60
commit 5997eee0d7
6 changed files with 700 additions and 10 deletions

View File

@@ -8,7 +8,7 @@
chat: 'You are a helpful, knowledgeable AI assistant. Be concise and accurate.',
coding: 'You are an expert coding assistant. Write clean, efficient, well-documented code. Always use markdown code blocks with language tags. Explain your approach briefly before and after code. Handle edge cases and errors properly.',
brainstorm: 'You are a creative brainstorming partner. Generate diverse ideas, explore unconventional angles, build on concepts, and help evaluate trade-offs. Think freely and expansively. Present ideas in organized lists or tables when appropriate.',
agentic: 'You are an autonomous coding agent 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.'
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 state = {
@@ -31,7 +31,9 @@
terminalOpen: false,
keepAwake: false,
autoDeploy: true,
maxRetries: 10
maxRetries: 10,
autoContinue: true,
maxAutoContinue: 5
};
function $(sel) { return document.querySelector(sel); }
@@ -52,6 +54,8 @@
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;
@@ -73,6 +77,8 @@
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); }
@@ -115,6 +121,7 @@
title: 'New Chat',
mode: state.currentMode,
messages: [],
files: [],
createdAt: Date.now()
};
state.conversations.unshift(conv);
@@ -218,6 +225,10 @@
return d.innerHTML;
}
function escapeAttr(text) {
return (text || '').replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function renderMarkdown(text) {
if (typeof marked !== 'undefined') {
marked.setOptions({
@@ -526,14 +537,11 @@
conv.messages.push({ role: 'assistant', content: state.streamingContent, _streaming: false });
}
} finally {
state.isGenerating = false;
state.abortController = null;
state.streamingConvId = null;
state.streamingResponseDiv = null;
updateSendButton();
saveState();
updateTerminalContent();
if (state.keepAwake) setWakeLock(false);
if (state.autoDeploy && (state.currentMode === 'coding' || state.currentMode === 'agentic')) {
var finalContent = state.streamingContent || '';
@@ -544,10 +552,20 @@
if (finalContent) {
var autoActions = parseAiActions(finalContent);
if (autoActions.length > 0) {
autoExecuteActions(autoActions, conv);
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);
}
}
@@ -1009,16 +1027,20 @@
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) {
@@ -1423,6 +1445,363 @@
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 += '<div class="ftree-folder">';
html += '<div class="ftree-node ftree-dir" style="padding-left:' + (indent + 8) + 'px">';
html += '<span class="ftree-arrow">&#9654;</span>';
html += '<span class="ftree-dirname">' + escapeHtml(dirs[d]) + '</span>';
html += '</div>';
html += '<div class="ftree-children">';
html += renderTreeHtml(child, convId, depth + 1);
html += '</div></div>';
}
for (var fi = 0; fi < node.files.length; fi++) {
var file = node.files[fi];
html += '<div class="ftree-node ftree-file" style="padding-left:' + (indent + 20) + 'px" ';
html += 'data-conv="' + convId + '" data-path="' + escapeAttr(file.path) + '">';
html += '<span class="ftree-ext">' + escapeHtml(file.language || '?') + '</span>';
html += '<span class="ftree-fname">' + escapeHtml(file.name) + '</span>';
html += '</div>';
}
return html;
}
function renderFileTree() {
var body = $('#file-tree-body');
if (!body) return;
var conv = getConversation();
if (!conv || !conv.files || conv.files.length === 0) {
body.innerHTML = '<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>';
var badge = $('#file-tree-count');
if (badge) badge.textContent = '0 files';
return;
}
var tree = buildFileTreeData(conv.files);
body.innerHTML = renderTreeHtml(tree, conv.id, 0);
var badge = $('#file-tree-count');
if (badge) badge.textContent = conv.files.length + ' file' + (conv.files.length !== 1 ? 's' : '');
}
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 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'; });
@@ -1994,6 +2373,9 @@
$('#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() {
@@ -2155,6 +2537,39 @@
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') ? '&#9660;' : '&#9654;';
}
}
});
$('#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() {