v2.1.0: File tree per session, auto-continue for incomplete tasks, task completion protocol
This commit is contained in:
427
www/js/app.js
427
www/js/app.js
@@ -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, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
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">▶</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') ? '▼' : '▶';
|
||||
}
|
||||
}
|
||||
});
|
||||
$('#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() {
|
||||
|
||||
Reference in New Issue
Block a user