v2.0.1: APK verification, stay awake fix, configurable auto-fix retries
This commit is contained in:
@@ -631,6 +631,11 @@ data: [DONE]
|
|||||||
|
|
||||||
## Changelog
|
## 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)
|
### v2.0.0 (2026-05-19)
|
||||||
- **Built-in Termux** — full Linux environment inside the app, no external Termux install needed
|
- **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)
|
- One-time ~30MB download of Termux bootstrap (bash, coreutils, apt, 25+ packages)
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "ai.z.chat"
|
applicationId "ai.z.chat"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 10
|
versionCode 11
|
||||||
versionName "2.0.0"
|
versionName "2.0.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import com.getcapacitor.annotation.CapacitorPlugin;
|
|||||||
|
|
||||||
@CapacitorPlugin(name = "Wake")
|
@CapacitorPlugin(name = "Wake")
|
||||||
public class WakePlugin extends Plugin {
|
public class WakePlugin extends Plugin {
|
||||||
private PowerManager.WakeLock wakeLock;
|
private PowerManager.WakeLock screenWakeLock;
|
||||||
|
private PowerManager.WakeLock cpuWakeLock;
|
||||||
private boolean isHeld = false;
|
private boolean isHeld = false;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -22,7 +23,7 @@ public class WakePlugin extends Plugin {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
public void acquire(PluginCall call) {
|
public void acquire(PluginCall call) {
|
||||||
if (isHeld && wakeLock != null) {
|
if (isHeld) {
|
||||||
call.resolve(new JSObject().put("held", true));
|
call.resolve(new JSObject().put("held", true));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -33,10 +34,20 @@ public class WakePlugin extends Plugin {
|
|||||||
});
|
});
|
||||||
|
|
||||||
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
|
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));
|
call.resolve(new JSObject().put("held", true));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
call.reject("Wake lock failed: " + e.getMessage());
|
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);
|
getActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (wakeLock != null && wakeLock.isHeld()) {
|
if (screenWakeLock != null && screenWakeLock.isHeld()) {
|
||||||
wakeLock.release();
|
screenWakeLock.release();
|
||||||
}
|
}
|
||||||
wakeLock = null;
|
screenWakeLock = null;
|
||||||
isHeld = false;
|
|
||||||
|
|
||||||
|
if (cpuWakeLock != null && cpuWakeLock.isHeld()) {
|
||||||
|
cpuWakeLock.release();
|
||||||
|
}
|
||||||
|
cpuWakeLock = null;
|
||||||
|
|
||||||
|
isHeld = false;
|
||||||
call.resolve(new JSObject().put("held", false));
|
call.resolve(new JSObject().put("held", false));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
call.reject("Wake release failed: " + e.getMessage());
|
call.reject("Wake release failed: " + e.getMessage());
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "1.3.0",
|
"version": "2.0.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "1.3.0",
|
"version": "2.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.3.4",
|
"@capacitor/android": "^8.3.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "zai-chat",
|
"name": "zai-chat",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
"description": "Z.AI Chat - Full stack AI chat powered by GLM Coding Plan",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -256,6 +256,11 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<span class="input-hint">Prevents screen sleep while agent is working</span>
|
<span class="input-hint">Prevents screen sleep while agent is working</span>
|
||||||
|
<div class="input-group" style="margin-top:12px">
|
||||||
|
<label>Max Auto-Fix Retries: <span id="retries-value">10</span></label>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Appearance</h3>
|
<h3>Appearance</h3>
|
||||||
@@ -274,13 +279,22 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>About</h3>
|
<h3>About</h3>
|
||||||
<p class="about-text">Z.AI Chat v2.0.0</p>
|
<p class="about-text">Z.AI Chat v2.0.1</p>
|
||||||
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
<p class="about-text">Built with Z.AI SDK & GLM-5.1</p>
|
||||||
<p class="about-text">Compatible with Android 15/16</p>
|
<p class="about-text">Compatible with Android 15/16</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<h3>Changelog</h3>
|
<h3>Changelog</h3>
|
||||||
<ul class="changelog-list">
|
<ul class="changelog-list">
|
||||||
|
<li>
|
||||||
|
<span class="changelog-version">v2.0.1</span>
|
||||||
|
<span class="changelog-date">2026-05-19</span>
|
||||||
|
<ul>
|
||||||
|
<li><strong>APK Verification</strong> — build output now verified: confirms APK file exists and shows size</li>
|
||||||
|
<li><strong>Stay Awake Fix</strong> — dual wake locks (screen bright + CPU) keep device fully awake during builds</li>
|
||||||
|
<li><strong>Configurable Retries</strong> — max auto-fix retries now adjustable (1–30) in Settings, default 10</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="changelog-version">v2.0.0</span>
|
<span class="changelog-version">v2.0.0</span>
|
||||||
<span class="changelog-date">2026-05-19</span>
|
<span class="changelog-date">2026-05-19</span>
|
||||||
|
|||||||
@@ -30,7 +30,8 @@
|
|||||||
streamingResponseDiv: null,
|
streamingResponseDiv: null,
|
||||||
terminalOpen: false,
|
terminalOpen: false,
|
||||||
keepAwake: false,
|
keepAwake: false,
|
||||||
autoDeploy: true
|
autoDeploy: true,
|
||||||
|
maxRetries: 10
|
||||||
};
|
};
|
||||||
|
|
||||||
function $(sel) { return document.querySelector(sel); }
|
function $(sel) { return document.querySelector(sel); }
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
|
state.terminalOpen = localStorage.getItem(STORAGE_KEY + 'terminalOpen') === 'true';
|
||||||
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
|
state.keepAwake = localStorage.getItem(STORAGE_KEY + 'keepAwake') === 'true';
|
||||||
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
|
state.autoDeploy = localStorage.getItem(STORAGE_KEY + 'autoDeploy') !== 'false';
|
||||||
|
state.maxRetries = parseInt(localStorage.getItem(STORAGE_KEY + 'maxRetries')) || 10;
|
||||||
var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
|
var convData = localStorage.getItem(STORAGE_KEY + 'conversations');
|
||||||
state.conversations = convData ? JSON.parse(convData) : [];
|
state.conversations = convData ? JSON.parse(convData) : [];
|
||||||
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
|
state.activeConversationId = localStorage.getItem(STORAGE_KEY + 'activeConv') || null;
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
|
localStorage.setItem(STORAGE_KEY + 'terminalOpen', state.terminalOpen.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
|
localStorage.setItem(STORAGE_KEY + 'keepAwake', state.keepAwake.toString());
|
||||||
localStorage.setItem(STORAGE_KEY + 'autoDeploy', state.autoDeploy.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 + 'conversations', JSON.stringify(state.conversations));
|
||||||
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
|
localStorage.setItem(STORAGE_KEY + 'activeConv', state.activeConversationId || '');
|
||||||
} catch(e) { console.error('Save state error:', e); }
|
} catch(e) { console.error('Save state error:', e); }
|
||||||
@@ -1415,7 +1418,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var _agenticRetryCount = 0;
|
var _agenticRetryCount = 0;
|
||||||
var MAX_AGENTIC_RETRIES = 3;
|
|
||||||
|
function getMaxRetries() {
|
||||||
|
return state.maxRetries || 10;
|
||||||
|
}
|
||||||
|
|
||||||
async function autoExecuteActions(actions, conv) {
|
async function autoExecuteActions(actions, conv) {
|
||||||
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
|
var hasFiles = actions.some(function(a) { return a.type === 'create_file'; });
|
||||||
@@ -1506,8 +1512,8 @@
|
|||||||
'AAPT2=$(which aapt2 2>/dev/null) && ' +
|
'AAPT2=$(which aapt2 2>/dev/null) && ' +
|
||||||
'D8=$(which d8 2>/dev/null) && ' +
|
'D8=$(which d8 2>/dev/null) && ' +
|
||||||
'ECJ=$(which ecj 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 "$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 Termux: pkg install ecj"; 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 && ' +
|
'mkdir -p build/gen build/classes && ' +
|
||||||
'echo "[*] Compiling resources..." && ' +
|
'echo "[*] Compiling resources..." && ' +
|
||||||
'$AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
|
'$AAPT2 compile --dir app/src/main/res -o build/compiled_resources.zip 2>&1 && ' +
|
||||||
@@ -1527,7 +1533,7 @@
|
|||||||
'else ' +
|
'else ' +
|
||||||
' DX=$(which dx 2>/dev/null) && ' +
|
' DX=$(which dx 2>/dev/null) && ' +
|
||||||
' if [ -n "$DX" ]; then $DX --output build/classes.dex build/classes/ 2>&1; ' +
|
' 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 && ' +
|
'fi && ' +
|
||||||
'echo "[*] Packaging..." && ' +
|
'echo "[*] Packaging..." && ' +
|
||||||
'cd build && ' +
|
'cd build && ' +
|
||||||
@@ -1546,37 +1552,41 @@
|
|||||||
'fi && ' +
|
'fi && ' +
|
||||||
'APK_PATH="' + projectDir + '/build/app-signed.apk" && ' +
|
'APK_PATH="' + projectDir + '/build/app-signed.apk" && ' +
|
||||||
'APK_SIZE=$(du -h app-signed.apk 2>/dev/null | cut -f1) && ' +
|
'APK_SIZE=$(du -h app-signed.apk 2>/dev/null | cut -f1) && ' +
|
||||||
'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)" && ' +
|
'echo "[BUILD OK] APK: $APK_PATH ($APK_SIZE)"';
|
||||||
'echo $APK_PATH';
|
|
||||||
|
|
||||||
var result = await shellExec(buildScript, termState.homeDir, false);
|
var result = await shellExec(buildScript, termState.homeDir, false);
|
||||||
var output = result.output || '';
|
var output = result.output || '';
|
||||||
termPrint(output.replace(/\n$/, ''), result.exitCode === 0 ? '' : 'err');
|
termPrint(output.replace(/\n$/, ''), result.exitCode === 0 ? '' : 'err');
|
||||||
|
|
||||||
if (output.indexOf('[BUILD OK]') >= 0) {
|
if (output.indexOf('[BUILD OK]') >= 0) {
|
||||||
var apkMatch = output.match(/\[BUILD OK\] APK: ([^\s]+)/);
|
var apkPath = projectDir + '/build/app-signed.apk';
|
||||||
if (apkMatch) {
|
var verifyResult = await shellExec('ls -la ' + apkPath + ' 2>&1', termState.homeDir, false);
|
||||||
termPrint('\nAPK ready: ' + apkMatch[1], 'success');
|
if (verifyResult.output && verifyResult.output.indexOf('No such file') < 0) {
|
||||||
termPrint('Run: install ' + apkMatch[1], 'info');
|
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) {
|
} else if (output.indexOf('[BUILD FAILED]') >= 0) {
|
||||||
return '[BUILD FAILED] ' + output;
|
return '[BUILD FAILED] ' + output;
|
||||||
} else if (result.exitCode !== 0) {
|
} 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);
|
return output.substring(0, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function agenticRetryOnError(errorOutput, conv) {
|
async function agenticRetryOnError(errorOutput, conv) {
|
||||||
_agenticRetryCount++;
|
_agenticRetryCount++;
|
||||||
if (_agenticRetryCount > MAX_AGENTIC_RETRIES) {
|
if (_agenticRetryCount > getMaxRetries()) {
|
||||||
termPrint('\n[!] Max retries reached (' + MAX_AGENTIC_RETRIES + '). Fix manually or ask again.', 'err');
|
termPrint('\n[!] Max retries reached (' + getMaxRetries() + '). Fix manually or ask again.', 'err');
|
||||||
showStatusToast('Build failed after ' + MAX_AGENTIC_RETRIES + ' retries', 'err');
|
showStatusToast('Build failed after ' + getMaxRetries() + ' retries', 'err');
|
||||||
return;
|
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');
|
showStatusToast('Build failed — AI auto-fixing (attempt ' + _agenticRetryCount + ')...', 'err');
|
||||||
|
|
||||||
if (!conv || !state.apiKey) return;
|
if (!conv || !state.apiKey) return;
|
||||||
@@ -1644,7 +1654,7 @@
|
|||||||
if (state.keepAwake) setWakeLock(false);
|
if (state.keepAwake) setWakeLock(false);
|
||||||
|
|
||||||
var fixActions = parseAiActions(state.streamingContent || '');
|
var fixActions = parseAiActions(state.streamingContent || '');
|
||||||
if (fixActions.length > 0 && _agenticRetryCount <= MAX_AGENTIC_RETRIES) {
|
if (fixActions.length > 0 && _agenticRetryCount <= getMaxRetries()) {
|
||||||
await autoExecuteActions(fixActions, conv);
|
await autoExecuteActions(fixActions, conv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1982,6 +1992,8 @@
|
|||||||
$('#settings-streaming').checked = state.streaming;
|
$('#settings-streaming').checked = state.streaming;
|
||||||
$('#settings-autodeploy').checked = state.autoDeploy;
|
$('#settings-autodeploy').checked = state.autoDeploy;
|
||||||
$('#settings-keepawake').checked = state.keepAwake;
|
$('#settings-keepawake').checked = state.keepAwake;
|
||||||
|
$('#settings-maxretries').value = state.maxRetries;
|
||||||
|
$('#retries-value').textContent = state.maxRetries;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
@@ -2135,6 +2147,14 @@
|
|||||||
saveState();
|
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);
|
$('#theme-toggle-header').addEventListener('click', toggleTheme);
|
||||||
|
|
||||||
$('#settings-darkmode').addEventListener('change', function() {
|
$('#settings-darkmode').addEventListener('change', function() {
|
||||||
|
|||||||
Reference in New Issue
Block a user