diff --git a/README.md b/README.md
index 4db4bef..3dbfd2f 100644
--- a/README.md
+++ b/README.md
@@ -631,6 +631,11 @@ data: [DONE]
## Changelog
+### 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
+- **Configurable Auto-Fix Retries** — max retries adjustable from 1–30 in Settings (default 10, was hardcoded 3)
+
### v2.0.0 (2026-05-19)
- **Built-in Termux** — full Linux environment inside the app, no external Termux install needed
- One-time ~30MB download of Termux bootstrap (bash, coreutils, apt, 25+ packages)
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3339d3b..30e385f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,8 +7,8 @@ android {
applicationId "ai.z.chat"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 10
- versionName "2.0.0"
+ versionCode 11
+ versionName "2.0.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
diff --git a/android/app/src/main/java/ai/z/chat/WakePlugin.java b/android/app/src/main/java/ai/z/chat/WakePlugin.java
index 0a26bde..9aacbd8 100644
--- a/android/app/src/main/java/ai/z/chat/WakePlugin.java
+++ b/android/app/src/main/java/ai/z/chat/WakePlugin.java
@@ -12,7 +12,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
@CapacitorPlugin(name = "Wake")
public class WakePlugin extends Plugin {
- private PowerManager.WakeLock wakeLock;
+ private PowerManager.WakeLock screenWakeLock;
+ private PowerManager.WakeLock cpuWakeLock;
private boolean isHeld = false;
@Override
@@ -22,7 +23,7 @@ public class WakePlugin extends Plugin {
@PluginMethod
public void acquire(PluginCall call) {
- if (isHeld && wakeLock != null) {
+ if (isHeld) {
call.resolve(new JSObject().put("held", true));
return;
}
@@ -33,10 +34,20 @@ public class WakePlugin extends Plugin {
});
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
- wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "zai-chat:wakelock");
- wakeLock.acquire(12 * 60 * 60 * 1000L);
- isHeld = true;
+ screenWakeLock = pm.newWakeLock(
+ PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE,
+ "zai-chat:screen"
+ );
+ screenWakeLock.acquire(24 * 60 * 60 * 1000L);
+
+ cpuWakeLock = pm.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK,
+ "zai-chat:cpu"
+ );
+ cpuWakeLock.acquire(24 * 60 * 60 * 1000L);
+
+ isHeld = true;
call.resolve(new JSObject().put("held", true));
} catch (Exception e) {
call.reject("Wake lock failed: " + e.getMessage());
@@ -50,12 +61,17 @@ public class WakePlugin extends Plugin {
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
});
- if (wakeLock != null && wakeLock.isHeld()) {
- wakeLock.release();
+ if (screenWakeLock != null && screenWakeLock.isHeld()) {
+ screenWakeLock.release();
}
- wakeLock = null;
- isHeld = false;
+ screenWakeLock = null;
+ if (cpuWakeLock != null && cpuWakeLock.isHeld()) {
+ cpuWakeLock.release();
+ }
+ cpuWakeLock = null;
+
+ isHeld = false;
call.resolve(new JSObject().put("held", false));
} catch (Exception e) {
call.reject("Wake release failed: " + e.getMessage());
diff --git a/package-lock.json b/package-lock.json
index b1c8300..7e14a5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "zai-chat",
- "version": "1.3.0",
+ "version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zai-chat",
- "version": "1.3.0",
+ "version": "2.0.1",
"license": "MIT",
"dependencies": {
"@capacitor/android": "^8.3.4",
diff --git a/package.json b/package.json
index 5140706..1cb9b21 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "zai-chat",
- "version": "2.0.0",
+ "version": "2.0.1",
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
"main": "index.js",
"scripts": {
diff --git a/www/index.html b/www/index.html
index e0f5e30..11e01dd 100644
--- a/www/index.html
+++ b/www/index.html
@@ -256,6 +256,11 @@
Prevents screen sleep while agent is working
+
+
+
+
+ How many times AI will auto-retry after build failures
Appearance
@@ -274,13 +279,22 @@
About
-
Z.AI Chat v2.0.0
+
Z.AI Chat v2.0.1
Built with Z.AI SDK & GLM-5.1
Compatible with Android 15/16
Changelog
+ -
+ v2.0.1
+ 2026-05-19
+
+ - APK Verification — build output now verified: confirms APK file exists and shows size
+ - Stay Awake Fix — dual wake locks (screen bright + CPU) keep device fully awake during builds
+ - Configurable Retries — max auto-fix retries now adjustable (1–30) in Settings, default 10
+
+
-
v2.0.0
2026-05-19
diff --git a/www/js/app.js b/www/js/app.js
index 32402c5..741c60a 100644
--- a/www/js/app.js
+++ b/www/js/app.js
@@ -30,7 +30,8 @@
streamingResponseDiv: null,
terminalOpen: false,
keepAwake: false,
- autoDeploy: true
+ autoDeploy: true,
+ maxRetries: 10
};
function $(sel) { return document.querySelector(sel); }
@@ -50,6 +51,7 @@
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;
var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
state.conversations = convData ? JSON.parse(convData) : [];
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
@@ -70,6 +72,7 @@
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 + 'conversations', JSON.stringify(state.conversations));
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
} catch(e) { console.error('Save state error:', e); }
@@ -1415,7 +1418,10 @@
}
var _agenticRetryCount = 0;
- var MAX_AGENTIC_RETRIES = 3;
+
+ function getMaxRetries() {
+ return state.maxRetries || 10;
+ }
async function autoExecuteActions(actions, conv) {
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
@@ -1506,8 +1512,8 @@
'AAPT2=$(which aapt2 2>/dev/null) && ' +
'D8=$(which d8 2>/dev/null) && ' +
'ECJ=$(which ecj 2>/dev/null) && ' +
- 'if [ -z "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found. Install Termux: pkg install aapt2"; exit 1; fi && ' +
- 'if [ -z "$ECJ" ]; then echo "[BUILD FAILED] ecj not found. Install Termux: pkg install ecj"; exit 1; fi && ' +
+ 'if [ -z "$AAPT2" ]; then echo "[BUILD FAILED] aapt2 not found. Install via: pkg install aapt2"; exit 1; fi && ' +
+ 'if [ -z "$ECJ" ]; then echo "[BUILD FAILED] ecj not found. Install via: pkg install ecj"; exit 1; fi && ' +
'mkdir -p build/gen build/classes && ' +
'echo "[*] Compiling resources..." && ' +
'$AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
@@ -1527,7 +1533,7 @@
'else ' +
' DX=$(which dx 2>/dev/null) && ' +
' if [ -n "$DX" ]; then $DX --output build/classes.dex build/classes/ 2>&1; ' +
- ' else echo "[BUILD FAILED] d8/dx not found. Install Termux: pkg install dx"; exit 1; fi; ' +
+ ' else echo "[BUILD FAILED] d8/dx not found. Install via: pkg install dx"; exit 1; fi; ' +
'fi && ' +
'echo "[*] Packaging..." && ' +
'cd build && ' +
@@ -1546,37 +1552,41 @@
'fi && ' +
'APK_PATH="' + projectDir + '/build/app-signed.apk" && ' +
'APK_SIZE=$(du -h app-signed.apk 2>/dev/null | cut -f1) && ' +
- 'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)" && ' +
- 'echo $APK_PATH';
+ 'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)"';
var result = await shellExec(buildScript, termState.homeDir, false);
var output = result.output || '';
termPrint(output.replace(/\n$/, ''), result.exitCode === 0 ? '' : 'err');
if (output.indexOf('[BUILD OK]') >= 0) {
- var apkMatch = output.match(/\[BUILD OK\] APK: ([^\s]+)/);
- if (apkMatch) {
- termPrint('\nAPK ready: ' + apkMatch[1], 'success');
- termPrint('Run: install ' + apkMatch[1], 'info');
+ 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);
}
- return '[BUILD OK] ' + output.substring(output.indexOf('[BUILD OK]'));
} 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, 1000);
+ return '[BUILD FAILED] exit=' + result.exitCode + '\n' + output.substring(0, 1500);
}
return output.substring(0, 500);
}
async function agenticRetryOnError(errorOutput, conv) {
_agenticRetryCount++;
- if (_agenticRetryCount > MAX_AGENTIC_RETRIES) {
- termPrint('\n[!] Max retries reached (' + MAX_AGENTIC_RETRIES + '). Fix manually or ask again.', 'err');
- showStatusToast('Build failed after ' + MAX_AGENTIC_RETRIES + ' retries', 'err');
+ 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 + '/' + MAX_AGENTIC_RETRIES + ')...', 'warning');
+ 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;
@@ -1644,7 +1654,7 @@
if (state.keepAwake) setWakeLock(false);
var fixActions = parseAiActions(state.streamingContent || '');
- if (fixActions.length > 0 && _agenticRetryCount <= MAX_AGENTIC_RETRIES) {
+ if (fixActions.length > 0 && _agenticRetryCount <= getMaxRetries()) {
await autoExecuteActions(fixActions, conv);
}
}
@@ -1982,6 +1992,8 @@
$('#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;
}
function saveSettings() {
@@ -2135,6 +2147,14 @@
saveState();
});
+ $('#settings-maxretries').addEventListener('input', function() {
+ $('#retries-value').textContent = this.value;
+ });
+ $('#settings-maxretries').addEventListener('change', function() {
+ state.maxRetries = parseInt(this.value) || 10;
+ saveState();
+ });
+
$('#theme-toggle-header').addEventListener('click', toggleTheme);
$('#settings-darkmode').addEventListener('change', function() {