diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index 9d5643b67..92c31cde8 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -6,11 +6,6 @@ import WebKit @MainActor @Observable final class ScreenController { - enum Mode: Sendable { - case canvas - case web - } - let webView: WKWebView private let navigationDelegate: ScreenNavigationDelegate private let a2uiActionHandler: CanvasA2UIActionMessageHandler @@ -18,10 +13,6 @@ final class ScreenController { var urlString: String = "" var errorText: String? - var mode: Mode { - self.urlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? .canvas : .web - } - /// Callback invoked when a clawdis:// deep link is tapped in the canvas var onDeepLink: ((URL) -> Void)? @@ -65,10 +56,15 @@ final class ScreenController { let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { guard let url = Self.canvasScaffoldURL else { return } + self.errorText = nil self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) return } else { - guard let url = URL(string: trimmed) else { return } + guard let url = URL(string: trimmed) else { + self.errorText = "Invalid URL: \(trimmed)" + return + } + self.errorText = nil if url.isFileURL { self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) } else { @@ -221,6 +217,10 @@ final class ScreenController { } if host == "localhost" { return true } if host.hasSuffix(".local") { return true } + if host.hasSuffix(".ts.net") { return true } + if host.hasSuffix(".tailscale.net") { return true } + // Allow MagicDNS / LAN hostnames like "peters-mac-studio-1". + if !host.contains("."), !host.contains(":") { return true } if let ipv4 = Self.parseIPv4(host) { return Self.isLocalNetworkIPv4(ipv4) } @@ -306,6 +306,22 @@ private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { decisionHandler(.allow) } + + func webView( + _: WKWebView, + didFailProvisionalNavigation _: WKNavigation?, + withError error: any Error) + { + Task { @MainActor in + self.controller?.errorText = error.localizedDescription + } + } + + func webView(_: WKWebView, didFail _: WKNavigation?, withError error: any Error) { + Task { @MainActor in + self.controller?.errorText = error.localizedDescription + } + } } private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHandler { diff --git a/apps/ios/Sources/Screen/ScreenTab.swift b/apps/ios/Sources/Screen/ScreenTab.swift index aff9320c7..e674b8e93 100644 --- a/apps/ios/Sources/Screen/ScreenTab.swift +++ b/apps/ios/Sources/Screen/ScreenTab.swift @@ -21,5 +21,5 @@ struct ScreenTab: View { } } - // Navigation/mode selection is agent-driven; no local controls here. + // Navigation is agent-driven; no local URL bar here. } diff --git a/apps/ios/Tests/ScreenControllerTests.swift b/apps/ios/Tests/ScreenControllerTests.swift index 775f5edcf..75dca0ec5 100644 --- a/apps/ios/Tests/ScreenControllerTests.swift +++ b/apps/ios/Tests/ScreenControllerTests.swift @@ -6,7 +6,6 @@ import WebKit @Test @MainActor func canvasModeConfiguresWebViewForTouch() { let screen = ScreenController() - #expect(screen.mode == .canvas) #expect(screen.webView.isOpaque == true) #expect(screen.webView.backgroundColor == .black) @@ -17,11 +16,11 @@ import WebKit #expect(scrollView.bounces == false) } - @Test @MainActor func navigateDefaultsToWebMode() { + @Test @MainActor func navigateSlashShowsDefaultCanvas() { let screen = ScreenController() - screen.navigate(to: "not a url") + screen.navigate(to: "/") - #expect(screen.mode == .web) + #expect(screen.urlString.isEmpty) } @Test @MainActor func evalExecutesJavaScript() async throws { @@ -46,6 +45,8 @@ import WebKit let screen = ScreenController() #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://localhost:18793/")!) == true) #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://clawd.local:18793/")!) == true) + #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://peters-mac-studio-1:18793/")!) == true) + #expect(screen.isLocalNetworkCanvasURL(URL(string: "https://peters-mac-studio-1.ts.net:18793/")!) == true) #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://192.168.0.10:18793/")!) == true) #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://10.0.0.10:18793/")!) == true) #expect(screen.isLocalNetworkCanvasURL(URL(string: "http://100.123.224.76:18793/")!) == true) // Tailscale CGNAT diff --git a/docs/ios/spec.md b/docs/ios/spec.md index d3cd71499..de37f36dd 100644 --- a/docs/ios/spec.md +++ b/docs/ios/spec.md @@ -151,7 +151,7 @@ When iOS is backgrounded: ### App structure - Single fullscreen Canvas surface (WKWebView). - One settings entry point: a **gear button** that opens a settings sheet. -- All navigation/mode selection is **agent-driven** (no local URL bar). +- All navigation is **agent-driven** (no local URL bar). ### Components - `BridgeDiscovery`: Bonjour browse + resolve (Network.framework `NWBrowser`) @@ -232,4 +232,4 @@ open Clawdis.xcodeproj ## Open questions - Should `connect.params.client.mode` be `"node"` with `platform="ios ..."` or a distinct mode `"ios-node"`? (Presence filtering currently excludes `"cli"` only.) - Do we want a “permissions” model per node (voice only vs voice+canvas) at pairing time? -- Should “website mode” allow arbitrary https, or enforce an allowlist to reduce risk? +- Should loading arbitrary websites via `canvas.navigate` allow any https URL, or enforce an allowlist to reduce risk?