diff --git a/apps/ios/Sources/RootCanvas.swift b/apps/ios/Sources/RootCanvas.swift index d0689a223..9a5fb0b76 100644 --- a/apps/ios/Sources/RootCanvas.swift +++ b/apps/ios/Sources/RootCanvas.swift @@ -8,6 +8,7 @@ struct RootCanvas: View { @Environment(\.scenePhase) private var scenePhase @AppStorage(VoiceWakePreferences.enabledKey) private var voiceWakeEnabled: Bool = false @AppStorage("screen.preventSleep") private var preventSleep: Bool = true + @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false @State private var presentedSheet: PresentedSheet? @State private var voiceWakeToastText: String? @State private var toastDismissTask: Task? @@ -56,6 +57,11 @@ struct RootCanvas: View { .onAppear { self.updateIdleTimer() } .onChange(of: self.preventSleep) { _, _ in self.updateIdleTimer() } .onChange(of: self.scenePhase) { _, _ in self.updateIdleTimer() } + .onAppear { self.updateCanvasDebugStatus() } + .onChange(of: self.canvasDebugStatusEnabled) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.bridgeStatusText) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.bridgeServerName) { _, _ in self.updateCanvasDebugStatus() } + .onChange(of: self.appModel.bridgeRemoteAddress) { _, _ in self.updateCanvasDebugStatus() } .onChange(of: self.voiceWake.lastTriggeredCommand) { _, newValue in guard let newValue else { return } let trimmed = newValue.trimmingCharacters(in: .whitespacesAndNewlines) @@ -102,6 +108,14 @@ struct RootCanvas: View { private func updateIdleTimer() { UIApplication.shared.isIdleTimerDisabled = (self.scenePhase == .active && self.preventSleep) } + + private func updateCanvasDebugStatus() { + self.appModel.screen.setDebugStatusEnabled(self.canvasDebugStatusEnabled) + guard self.canvasDebugStatusEnabled else { return } + let title = self.appModel.bridgeStatusText.trimmingCharacters(in: .whitespacesAndNewlines) + let subtitle = self.appModel.bridgeServerName ?? self.appModel.bridgeRemoteAddress + self.appModel.screen.updateDebugStatus(title: title, subtitle: subtitle) + } } private struct CanvasContent: View { diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index e808bab9a..e61be9a6e 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -19,6 +19,10 @@ final class ScreenController { /// Callback invoked when the user clicks an A2UI action (e.g. button) inside the canvas web UI. var onA2UIAction: (([String: Any]) -> Void)? + private var debugStatusEnabled: Bool = false + private var debugStatusTitle: String? + private var debugStatusSubtitle: String? + init() { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() @@ -80,6 +84,39 @@ final class ScreenController { self.reload() } + func setDebugStatusEnabled(_ enabled: Bool) { + self.debugStatusEnabled = enabled + self.applyDebugStatusIfNeeded() + } + + func updateDebugStatus(title: String?, subtitle: String?) { + self.debugStatusTitle = title + self.debugStatusSubtitle = subtitle + self.applyDebugStatusIfNeeded() + } + + fileprivate func applyDebugStatusIfNeeded() { + let enabled = self.debugStatusEnabled + let title = self.debugStatusTitle + let subtitle = self.debugStatusSubtitle + let js = """ + (() => { + try { + const api = globalThis.__clawdis; + if (!api) return; + if (typeof api.setDebugStatusEnabled === 'function') { + api.setDebugStatusEnabled(\(enabled ? "true" : "false")); + } + if (!\(enabled ? "true" : "false")) return; + if (typeof api.setStatus === 'function') { + api.setStatus(\(Self.jsValue(title)), \(Self.jsValue(subtitle))); + } + } catch (_) {} + })() + """ + self.webView.evaluateJavaScript(js) { _, _ in } + } + func waitForA2UIReady(timeoutMs: Int) async -> Bool { let clock = ContinuousClock() let deadline = clock.now.advanced(by: .milliseconds(timeoutMs)) @@ -212,6 +249,17 @@ final class ScreenController { return false } + private static func jsValue(_ value: String?) -> String { + guard let value else { return "null" } + if let data = try? JSONSerialization.data(withJSONObject: [value]), + let encoded = String(data: data, encoding: .utf8), + encoded.count >= 2 + { + return String(encoded.dropFirst().dropLast()) + } + return "null" + } + func isLocalNetworkCanvasURL(_ url: URL) -> Bool { guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else { return false @@ -320,6 +368,7 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { func webView(_: WKWebView, didFinish _: WKNavigation?) { self.controller?.errorText = nil + self.controller?.applyDebugStatusIfNeeded() } func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { diff --git a/apps/ios/Sources/Settings/SettingsTab.swift b/apps/ios/Sources/Settings/SettingsTab.swift index 5707d211d..48b5e0aac 100644 --- a/apps/ios/Sources/Settings/SettingsTab.swift +++ b/apps/ios/Sources/Settings/SettingsTab.swift @@ -28,6 +28,7 @@ struct SettingsTab: View { @AppStorage("bridge.manual.host") private var manualBridgeHost: String = "" @AppStorage("bridge.manual.port") private var manualBridgePort: Int = 18790 @AppStorage("bridge.discovery.debugLogs") private var discoveryDebugLogsEnabled: Bool = false + @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false @State private var connectStatus = ConnectStatusStore() @State private var connectingBridgeID: String? @State private var localIPAddress: String? @@ -142,6 +143,8 @@ struct SettingsTab: View { NavigationLink("Discovery Logs") { BridgeDiscoveryDebugLogView() } + + Toggle("Debug Canvas Status", isOn: self.$canvasDebugStatusEnabled) } }