import ClawdisKit import Observation import SwiftUI import WebKit @MainActor @Observable final class ScreenController { let webView: WKWebView private let navigationDelegate: ScreenNavigationDelegate var mode: ClawdisCanvasMode = .canvas var urlString: String = "" var errorText: String? /// Callback invoked when a clawdis:// deep link is tapped in the canvas var onDeepLink: ((URL) -> Void)? init() { let config = WKWebViewConfiguration() config.websiteDataStore = .nonPersistent() self.navigationDelegate = ScreenNavigationDelegate() self.webView = WKWebView(frame: .zero, configuration: config) self.webView.isOpaque = false self.webView.backgroundColor = .clear self.webView.scrollView.backgroundColor = .clear self.webView.scrollView.contentInsetAdjustmentBehavior = .never self.webView.scrollView.contentInset = .zero self.webView.scrollView.scrollIndicatorInsets = .zero self.webView.scrollView.automaticallyAdjustsScrollIndicatorInsets = false // Disable scroll to allow touch events to pass through to canvas self.webView.scrollView.isScrollEnabled = false self.webView.scrollView.bounces = false self.webView.navigationDelegate = self.navigationDelegate self.navigationDelegate.controller = self self.reload() } func setMode(_ mode: ClawdisCanvasMode) { self.mode = mode self.reload() } func navigate(to urlString: String) { self.urlString = urlString self.reload() } func reload() { switch self.mode { case .web: guard let url = URL(string: self.urlString.trimmingCharacters(in: .whitespacesAndNewlines)) else { return } self.webView.load(URLRequest(url: url)) case .canvas: self.webView.loadHTMLString(Self.canvasScaffoldHTML, baseURL: nil) } } func eval(javaScript: String) async throws -> String { try await withCheckedThrowingContinuation { cont in self.webView.evaluateJavaScript(javaScript) { result, error in if let error { cont.resume(throwing: error) return } if let result { cont.resume(returning: String(describing: result)) } else { cont.resume(returning: "") } } } } func snapshotPNGBase64(maxWidth: CGFloat? = nil) async throws -> String { let config = WKSnapshotConfiguration() if let maxWidth { config.snapshotWidth = NSNumber(value: Double(maxWidth)) } let image: UIImage = try await withCheckedThrowingContinuation { cont in self.webView.takeSnapshot(with: config) { image, error in if let error { cont.resume(throwing: error) return } guard let image else { cont.resume(throwing: NSError(domain: "Screen", code: 2, userInfo: [ NSLocalizedDescriptionKey: "snapshot failed", ])) return } cont.resume(returning: image) } } guard let data = image.pngData() else { throw NSError(domain: "Screen", code: 1, userInfo: [ NSLocalizedDescriptionKey: "snapshot encode failed", ]) } return data.base64EncodedString() } private static let canvasScaffoldHTML = """ Canvas
Ready
Waiting for agent
""" } // MARK: - Navigation Delegate /// Handles navigation policy to intercept clawdis:// deep links from canvas private final class ScreenNavigationDelegate: NSObject, WKNavigationDelegate { weak var controller: ScreenController? func webView( _ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url else { decisionHandler(.allow) return } // Intercept clawdis:// deep links if url.scheme == "clawdis" { decisionHandler(.cancel) Task { @MainActor in self.controller?.onDeepLink?(url) } return } decisionHandler(.allow) } }