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

@@ -631,6 +631,14 @@ data: [DONE]
## Changelog
### v2.1.0 (2026-05-19)
- **File Tree per Session** — sidebar shows all AI-generated files per conversation, click to view/edit/save
- **File Viewer with Edit Mode** — view any file, switch to edit mode, save changes back to device
- **Auto-Continue** — app detects incomplete AI responses and auto-continues until task is confirmed done
- **Task Completion Protocol** — AI outputs `[TASK_COMPLETE]` when done; detects truncated code blocks and unclosed file tags
- **Configurable Auto-Continue** — max auto-continues adjustable 120 (default 5) in Settings
- File tree button visible in Coding/Agentic modes alongside Terminal toggle
### v2.0.1 (2026-05-19)
- **APK Build Verification** — confirms APK file exists after build, shows file size
- **Stay Awake Fix** — dual wake locks (screen bright + CPU partial) with 24h timeout keep device fully awake

View File

@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 11
versionName "2.0.1"
versionCode 12
versionName "2.1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'

View File

@@ -1,6 +1,6 @@
{
"name": "zai-chat",
"version": "2.0.1",
"version": "2.1.0",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js",
"scripts": {

View File

@@ -899,6 +899,214 @@ a:hover { text-decoration: underline; }
font-size: 11px;
}
/* File Tree Panel */
.file-tree-panel {
position: fixed;
right: -300px;
top: 0;
bottom: 0;
width: 280px;
background: var(--bg-secondary);
border-left: 1px solid var(--border);
z-index: 100;
display: flex;
flex-direction: column;
transition: right 0.3s ease;
}
.file-tree-panel.open { right: 0; }
.file-tree-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--sidebar-overlay-bg);
z-index: 99;
display: none;
}
.file-tree-overlay.active { display: block; }
.file-tree-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--border);
}
.file-tree-header h3 { font-size: 16px; }
.file-tree-body {
flex: 1;
overflow-y: auto;
padding: 8px 0;
}
.file-tree-footer {
padding: 8px 16px;
border-top: 1px solid var(--border);
font-size: 12px;
color: var(--text-muted);
}
.ftree-empty {
text-align: center;
color: var(--text-muted);
font-size: 13px;
padding: 40px 20px;
line-height: 1.6;
}
.ftree-folder { }
.ftree-folder .ftree-children {
display: none;
}
.ftree-folder.open > .ftree-children {
display: block;
}
.ftree-node {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
cursor: pointer;
transition: background var(--transition);
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ftree-node:hover {
background: var(--accent-dim);
}
.ftree-dir {
color: var(--text-primary);
font-weight: 600;
}
.ftree-arrow {
font-size: 8px;
color: var(--text-muted);
width: 10px;
flex-shrink: 0;
transition: transform 0.2s ease;
}
.ftree-dirname {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
.ftree-file {
color: var(--text-secondary);
}
.ftree-ext {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
background: var(--accent-dim);
color: var(--accent);
padding: 1px 5px;
border-radius: 3px;
flex-shrink: 0;
letter-spacing: 0.3px;
}
.ftree-fname {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
/* File Viewer */
.file-viewer {
display: none;
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
z-index: 200;
flex-direction: column;
background: var(--bg-primary);
}
.file-viewer-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border);
min-height: 52px;
gap: 8px;
}
.file-viewer-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
}
#file-viewer-name {
font-size: 14px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fv-lang {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
background: var(--accent-dim);
color: var(--accent);
padding: 2px 6px;
border-radius: 3px;
flex-shrink: 0;
}
.file-viewer-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.fv-btn {
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 5px 14px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.fv-btn:active { background: var(--accent-dim); }
.fv-btn-save {
background: var(--success);
color: white;
border-color: var(--success);
}
.fv-btn-save:active { opacity: 0.8; }
.file-viewer-body {
flex: 1;
overflow: auto;
padding: 0;
}
.file-viewer-body pre {
margin: 0;
padding: 16px;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.5;
color: var(--text-primary);
white-space: pre-wrap;
word-break: break-all;
background: transparent;
}
.file-viewer-editor {
flex: 1;
display: flex;
}
.file-viewer-editor textarea {
flex: 1;
width: 100%;
padding: 16px;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 13px;
line-height: 1.5;
color: var(--text-primary);
background: var(--bg-code);
border: none;
outline: none;
resize: none;
-webkit-overflow-scrolling: touch;
}
/* Responsive */
@media (max-width: 480px) {
.message { max-width: 92%; }

View File

@@ -76,6 +76,7 @@
</div>
</div>
<div class="header-right">
<button id="file-tree-btn" class="icon-btn" title="Project Files" style="display:none">&#128193;</button>
<button id="theme-toggle-header" class="theme-toggle-btn" title="Toggle theme">&#9790;</button>
<button id="new-chat-btn" class="icon-btn" title="New chat">+</button>
<button id="settings-btn" class="icon-btn" title="Settings">&#9881;</button>
@@ -94,6 +95,40 @@
</div>
<div id="sidebar-overlay" class="sidebar-overlay"></div>
<div id="file-tree-panel" class="file-tree-panel">
<div class="file-tree-header">
<h3>Project Files</h3>
<button id="file-tree-close" class="icon-btn">&times;</button>
</div>
<div id="file-tree-body" class="file-tree-body">
<div class="ftree-empty">No files yet.<br>AI-generated files appear here.</div>
</div>
<div class="file-tree-footer">
<span id="file-tree-count">0 files</span>
</div>
</div>
<div id="file-tree-overlay" class="file-tree-overlay"></div>
<div id="file-viewer" class="file-viewer">
<div class="file-viewer-header">
<div class="file-viewer-title">
<span id="file-viewer-name">file</span>
<span id="file-viewer-lang" class="fv-lang"></span>
</div>
<div class="file-viewer-actions">
<button id="file-viewer-edit" class="fv-btn">Edit</button>
<button id="file-viewer-save" class="fv-btn fv-btn-save" style="display:none">Save</button>
<button id="file-viewer-close" class="icon-btn">&times;</button>
</div>
</div>
<div id="file-viewer-body" class="file-viewer-body">
<pre id="file-viewer-content"></pre>
</div>
<div id="file-viewer-editor" class="file-viewer-editor" style="display:none">
<textarea id="file-viewer-textarea"></textarea>
</div>
</div>
<div id="messages" class="messages"></div>
<div id="terminal-panel" class="terminal-panel">
@@ -261,6 +296,19 @@
<input type="range" id="settings-maxretries" min="1" max="30" step="1" value="10">
</div>
<span class="input-hint">How many times AI will auto-retry after build failures</span>
<div class="input-group toggle-group" style="margin-top:12px">
<label>Auto-Continue</label>
<label class="toggle">
<input type="checkbox" id="settings-autocontinue" checked>
<span class="toggle-slider"></span>
</label>
</div>
<span class="input-hint">Auto-continues AI if task is incomplete or response cut off</span>
<div class="input-group" style="margin-top:12px">
<label>Max Auto-Continues: <span id="autocont-value">5</span></label>
<input type="range" id="settings-maxautocontinue" min="1" max="20" step="1" value="5">
</div>
<span class="input-hint">Max times AI will be asked to continue incomplete work</span>
</div>
<div class="settings-section">
<h3>Appearance</h3>
@@ -279,13 +327,24 @@
</div>
<div class="settings-section">
<h3>About</h3>
<p class="about-text">Z.AI Chat v2.0.1</p>
<p class="about-text">Z.AI Chat v2.1.0</p>
<p class="about-text">Built with Z.AI SDK &amp; GLM-5.1</p>
<p class="about-text">Compatible with Android 15/16</p>
</div>
<div class="settings-section">
<h3>Changelog</h3>
<ul class="changelog-list">
<li>
<span class="changelog-version">v2.1.0</span>
<span class="changelog-date">2026-05-19</span>
<ul>
<li><strong>File Tree</strong> — per-session file tree shows all AI-generated files, view/edit/save from sidebar</li>
<li><strong>Auto-Continue</strong> — if AI stops mid-task, app auto-continues until task is confirmed complete</li>
<li><strong>Task Completion Protocol</strong> — AI outputs [TASK_COMPLETE] when done; app detects truncated responses</li>
<li>File viewer with edit mode — modify and save files back to device</li>
<li>Configurable max auto-continues (120, default 5) in Settings</li>
</ul>
</li>
<li>
<span class="changelog-version">v2.0.1</span>
<span class="changelog-date">2026-05-19</span>

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() {