From 7932e966db831c5538c58e1ba46af50d62d22960 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Dec 2025 14:20:22 +0100 Subject: [PATCH] feat(android): toggle debug canvas status --- .../main/assets/CanvasScaffold/scaffold.html | 190 ++++++++++++++++++ .../steipete/clawdis/node/MainViewModel.kt | 5 + .../com/steipete/clawdis/node/NodeRuntime.kt | 21 ++ .../com/steipete/clawdis/node/SecurePrefs.kt | 9 + .../clawdis/node/node/CanvasController.kt | 46 +++++ .../steipete/clawdis/node/ui/RootScreen.kt | 4 + .../steipete/clawdis/node/ui/SettingsSheet.kt | 18 ++ 7 files changed, 293 insertions(+) create mode 100644 apps/android/app/src/main/assets/CanvasScaffold/scaffold.html diff --git a/apps/android/app/src/main/assets/CanvasScaffold/scaffold.html b/apps/android/app/src/main/assets/CanvasScaffold/scaffold.html new file mode 100644 index 000000000..6c77189ec --- /dev/null +++ b/apps/android/app/src/main/assets/CanvasScaffold/scaffold.html @@ -0,0 +1,190 @@ + + + + + + Canvas + + + + +
+
+
Ready
+
Waiting for agent
+
+
+ + + diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt index 245e0156c..28d702975 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/MainViewModel.kt @@ -38,6 +38,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { val manualEnabled: StateFlow = runtime.manualEnabled val manualHost: StateFlow = runtime.manualHost val manualPort: StateFlow = runtime.manualPort + val canvasDebugStatusEnabled: StateFlow = runtime.canvasDebugStatusEnabled val chatSessionKey: StateFlow = runtime.chatSessionKey val chatSessionId: StateFlow = runtime.chatSessionId @@ -78,6 +79,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) { runtime.setManualPort(value) } + fun setCanvasDebugStatusEnabled(value: Boolean) { + runtime.setCanvasDebugStatusEnabled(value) + } + fun setWakeWords(words: List) { runtime.setWakeWords(words) } diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt index f711a4178..7cfd8c53f 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/NodeRuntime.kt @@ -166,6 +166,7 @@ class NodeRuntime(context: Context) { val manualHost: StateFlow = prefs.manualHost val manualPort: StateFlow = prefs.manualPort val lastDiscoveredStableId: StateFlow = prefs.lastDiscoveredStableId + val canvasDebugStatusEnabled: StateFlow = prefs.canvasDebugStatusEnabled private var didAutoConnect = false private var suppressWakeWordsSync = false @@ -246,6 +247,22 @@ class NodeRuntime(context: Context) { connect(target) } } + + scope.launch { + combine( + canvasDebugStatusEnabled, + statusText, + serverName, + remoteAddress, + ) { debugEnabled, status, server, remote -> + Quad(debugEnabled, status, server, remote) + }.distinctUntilChanged() + .collect { (debugEnabled, status, server, remote) -> + canvas.setDebugStatusEnabled(debugEnabled) + if (!debugEnabled) return@collect + canvas.setDebugStatus(status, server ?: remote) + } + } } fun setForeground(value: Boolean) { @@ -276,6 +293,10 @@ class NodeRuntime(context: Context) { prefs.setManualPort(value) } + fun setCanvasDebugStatusEnabled(value: Boolean) { + prefs.setCanvasDebugStatusEnabled(value) + } + fun setWakeWords(words: List) { prefs.setWakeWords(words) scheduleWakeWordsSyncIfNeeded() diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt index a0be15e13..8d7ceb0a2 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/SecurePrefs.kt @@ -63,6 +63,10 @@ class SecurePrefs(context: Context) { MutableStateFlow(prefs.getString("bridge.lastDiscoveredStableId", "")!!) val lastDiscoveredStableId: StateFlow = _lastDiscoveredStableId + private val _canvasDebugStatusEnabled = + MutableStateFlow(prefs.getBoolean("canvas.debugStatusEnabled", false)) + val canvasDebugStatusEnabled: StateFlow = _canvasDebugStatusEnabled + private val _wakeWords = MutableStateFlow(loadWakeWords()) val wakeWords: StateFlow> = _wakeWords @@ -107,6 +111,11 @@ class SecurePrefs(context: Context) { _manualPort.value = value } + fun setCanvasDebugStatusEnabled(value: Boolean) { + prefs.edit { putBoolean("canvas.debugStatusEnabled", value) } + _canvasDebugStatusEnabled.value = value + } + fun loadBridgeToken(): String? { val key = "bridge.token.${_instanceId.value}" return prefs.getString(key, null) diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/node/CanvasController.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/node/CanvasController.kt index d89dc83cb..5b4a09b64 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/node/CanvasController.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/node/CanvasController.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream import android.util.Base64 +import org.json.JSONObject import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -25,6 +26,9 @@ class CanvasController { @Volatile private var webView: WebView? = null @Volatile private var url: String? = null + @Volatile private var debugStatusEnabled: Boolean = false + @Volatile private var debugStatusTitle: String? = null + @Volatile private var debugStatusSubtitle: String? = null private val scaffoldAssetUrl = "file:///android_asset/CanvasScaffold/scaffold.html" @@ -36,6 +40,7 @@ class CanvasController { fun attach(webView: WebView) { this.webView = webView reload() + applyDebugStatus() } fun navigate(url: String) { @@ -48,6 +53,21 @@ class CanvasController { fun isDefaultCanvas(): Boolean = url == null + fun setDebugStatusEnabled(enabled: Boolean) { + debugStatusEnabled = enabled + applyDebugStatus() + } + + fun setDebugStatus(title: String?, subtitle: String?) { + debugStatusTitle = title + debugStatusSubtitle = subtitle + applyDebugStatus() + } + + fun onPageFinished() { + applyDebugStatus() + } + private inline fun withWebViewOnMain(crossinline block: (WebView) -> Unit) { val wv = webView ?: return if (Looper.myLooper() == Looper.getMainLooper()) { @@ -68,6 +88,32 @@ class CanvasController { } } + private fun applyDebugStatus() { + val enabled = debugStatusEnabled + val title = debugStatusTitle + val subtitle = debugStatusSubtitle + withWebViewOnMain { wv -> + val titleJs = title?.let { JSONObject.quote(it) } ?: "null" + val subtitleJs = subtitle?.let { JSONObject.quote(it) } ?: "null" + val js = """ + (() => { + try { + const api = globalThis.__clawdis; + if (!api) return; + if (typeof api.setDebugStatusEnabled === 'function') { + api.setDebugStatusEnabled(${if (enabled) "true" else "false"}); + } + if (!${if (enabled) "true" else "false"}) return; + if (typeof api.setStatus === 'function') { + api.setStatus($titleJs, $subtitleJs); + } + } catch (_) {} + })(); + """.trimIndent() + wv.evaluateJavascript(js, null) + } + } + suspend fun eval(javaScript: String): String = withContext(Dispatchers.Main) { val wv = webView ?: throw IllegalStateException("no webview") diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt index 38b61ed63..4ee3afa1a 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/RootScreen.kt @@ -187,6 +187,10 @@ private fun CanvasView(viewModel: MainViewModel, modifier: Modifier = Modifier) "onReceivedHttpError: ${errorResponse.statusCode} ${errorResponse.reasonPhrase} ${request.url}", ) } + + override fun onPageFinished(view: WebView, url: String?) { + viewModel.canvas.onPageFinished() + } } setBackgroundColor(Color.BLACK) setLayerType(View.LAYER_TYPE_HARDWARE, null) diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt index 899817639..038ef9faf 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/ui/SettingsSheet.kt @@ -64,6 +64,7 @@ fun SettingsSheet(viewModel: MainViewModel) { val manualEnabled by viewModel.manualEnabled.collectAsState() val manualHost by viewModel.manualHost.collectAsState() val manualPort by viewModel.manualPort.collectAsState() + val canvasDebugStatusEnabled by viewModel.canvasDebugStatusEnabled.collectAsState() val statusText by viewModel.statusText.collectAsState() val serverName by viewModel.serverName.collectAsState() val remoteAddress by viewModel.remoteAddress.collectAsState() @@ -394,6 +395,23 @@ fun SettingsSheet(viewModel: MainViewModel) { ) } + item { HorizontalDivider() } + + // Debug + item { Text("Debug", style = MaterialTheme.typography.titleSmall) } + item { + ListItem( + headlineContent = { Text("Debug Canvas Status") }, + supportingContent = { Text("Show status text in the canvas when debug is enabled.") }, + trailingContent = { + Switch( + checked = canvasDebugStatusEnabled, + onCheckedChange = viewModel::setCanvasDebugStatusEnabled, + ) + }, + ) + } + item { Spacer(modifier = Modifier.height(20.dp)) } } }