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 ce70838eb..1e8ea08d1 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 @@ -272,7 +272,6 @@ class NodeRuntime(context: Context) { buildList { add(ClawdisCanvasCommand.Show.rawValue) add(ClawdisCanvasCommand.Hide.rawValue) - add(ClawdisCanvasCommand.SetMode.rawValue) add(ClawdisCanvasCommand.Navigate.rawValue) add(ClawdisCanvasCommand.Eval.rawValue) add(ClawdisCanvasCommand.Snapshot.rawValue) @@ -544,14 +543,9 @@ class NodeRuntime(context: Context) { return when (command) { ClawdisCanvasCommand.Show.rawValue -> BridgeSession.InvokeResult.ok(null) ClawdisCanvasCommand.Hide.rawValue -> BridgeSession.InvokeResult.ok(null) - ClawdisCanvasCommand.SetMode.rawValue -> { - val mode = CanvasController.parseMode(paramsJson) - canvas.setMode(mode) - BridgeSession.InvokeResult.ok(null) - } ClawdisCanvasCommand.Navigate.rawValue -> { val url = CanvasController.parseNavigateUrl(paramsJson) - if (url != null) canvas.navigate(url) + canvas.navigate(url) BridgeSession.InvokeResult.ok(null) } ClawdisCanvasCommand.Eval.rawValue -> { @@ -638,7 +632,8 @@ class NodeRuntime(context: Context) { // ignore } - canvas.navigate(a2uiIndexUrl) + // Ensure the default canvas scaffold is loaded; A2UI is now hosted there. + canvas.navigate("") repeat(50) { try { val ready = canvas.eval(a2uiReadyCheckJS) @@ -713,8 +708,6 @@ class NodeRuntime(context: Context) { private data class Quad(val first: A, val second: B, val third: C, val fourth: D) -private const val a2uiIndexUrl: String = "file:///android_asset/CanvasA2UI/index.html" - private const val a2uiReadyCheckJS: String = """ (() => { 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 17f197664..2635cfbd5 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 @@ -14,11 +14,8 @@ import android.util.Base64 import kotlin.coroutines.resume class CanvasController { - enum class Mode { CANVAS, WEB } - @Volatile private var webView: WebView? = null - @Volatile private var mode: Mode = Mode.CANVAS - @Volatile private var url: String = "" + @Volatile private var url: String? = null private val scaffoldAssetUrl = "file:///android_asset/CanvasScaffold/scaffold.html" @@ -27,17 +24,9 @@ class CanvasController { reload() } - fun setMode(mode: Mode) { - this.mode = mode - reload() - } - fun navigate(url: String) { - this.url = url - if (url.trim().isNotBlank()) { - // `canvas.navigate` is expected to show web content; default to WEB mode to match iOS. - this.mode = Mode.WEB - } + val trimmed = url.trim() + this.url = if (trimmed.isBlank() || trimmed == "/") null else trimmed reload() } @@ -51,17 +40,12 @@ class CanvasController { } private fun reload() { - val currentMode = mode val currentUrl = url withWebViewOnMain { wv -> - when (currentMode) { - Mode.WEB -> { - // Match iOS behavior: if URL is missing/invalid, keep the current page (canvas scaffold). - val trimmed = currentUrl.trim() - if (trimmed.isBlank()) return@withWebViewOnMain - wv.loadUrl(trimmed) - } - Mode.CANVAS -> wv.loadUrl(scaffoldAssetUrl) + if (currentUrl == null) { + wv.loadUrl(scaffoldAssetUrl) + } else { + wv.loadUrl(currentUrl) } } } @@ -106,19 +90,9 @@ class CanvasController { } companion object { - fun parseMode(paramsJson: String?): Mode { - val obj = parseParamsObject(paramsJson) ?: return Mode.CANVAS - return if (obj.optString("mode", "").equals("web", ignoreCase = true)) { - Mode.WEB - } else { - Mode.CANVAS - } - } - - fun parseNavigateUrl(paramsJson: String?): String? { - val obj = parseParamsObject(paramsJson) ?: return null - val url = obj.optString("url", "").trim() - return url.takeIf { it.isNotBlank() } + fun parseNavigateUrl(paramsJson: String?): String { + val obj = parseParamsObject(paramsJson) + return obj?.optString("url", "")?.trim().orEmpty() } fun parseEvalJs(paramsJson: String?): String? { diff --git a/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt index e21557428..6494d5c79 100644 --- a/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt +++ b/apps/android/app/src/main/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstants.kt @@ -9,7 +9,6 @@ enum class ClawdisCapability(val rawValue: String) { enum class ClawdisCanvasCommand(val rawValue: String) { Show("canvas.show"), Hide("canvas.hide"), - SetMode("canvas.setMode"), Navigate("canvas.navigate"), Eval("canvas.eval"), Snapshot("canvas.snapshot"), diff --git a/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt index 07ec2c9d2..5ea73b4b1 100644 --- a/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt +++ b/apps/android/app/src/test/java/com/steipete/clawdis/node/protocol/ClawdisProtocolConstantsTest.kt @@ -8,7 +8,6 @@ class ClawdisProtocolConstantsTest { fun canvasCommandsUseStableStrings() { assertEquals("canvas.show", ClawdisCanvasCommand.Show.rawValue) assertEquals("canvas.hide", ClawdisCanvasCommand.Hide.rawValue) - assertEquals("canvas.setMode", ClawdisCanvasCommand.SetMode.rawValue) assertEquals("canvas.navigate", ClawdisCanvasCommand.Navigate.rawValue) assertEquals("canvas.eval", ClawdisCanvasCommand.Eval.rawValue) assertEquals("canvas.snapshot", ClawdisCanvasCommand.Snapshot.rawValue) diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index ca90109d9..49e3af0bd 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -378,11 +378,6 @@ final class NodeAppModel { case ClawdisCanvasCommand.hide.rawValue: return BridgeInvokeResponse(id: req.id, ok: true) - case ClawdisCanvasCommand.setMode.rawValue: - let params = try Self.decodeParams(ClawdisCanvasSetModeParams.self, from: req.paramsJSON) - self.screen.setMode(params.mode) - return BridgeInvokeResponse(id: req.id, ok: true) - case ClawdisCanvasCommand.navigate.rawValue: let params = try Self.decodeParams(ClawdisCanvasNavigateParams.self, from: req.paramsJSON) self.screen.navigate(to: params.url) @@ -402,7 +397,7 @@ final class NodeAppModel { return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) case ClawdisCanvasA2UICommand.reset.rawValue: - try self.screen.showA2UI() + self.screen.showDefaultCanvas() if await !self.screen.waitForA2UIReady(timeoutMs: 5000) { return BridgeInvokeResponse( id: req.id, @@ -434,7 +429,7 @@ final class NodeAppModel { } } - try self.screen.showA2UI() + self.screen.showDefaultCanvas() if await !self.screen.waitForA2UIReady(timeoutMs: 5000) { return BridgeInvokeResponse( id: req.id, diff --git a/apps/ios/Sources/Screen/ScreenController.swift b/apps/ios/Sources/Screen/ScreenController.swift index 1bff3eade..58ef6eab0 100644 --- a/apps/ios/Sources/Screen/ScreenController.swift +++ b/apps/ios/Sources/Screen/ScreenController.swift @@ -10,7 +10,6 @@ final class ScreenController { private let navigationDelegate: ScreenNavigationDelegate private let a2uiActionHandler: CanvasA2UIActionMessageHandler - var mode: ClawdisCanvasMode = .canvas var urlString: String = "" var errorText: String? @@ -47,45 +46,30 @@ final class ScreenController { self.reload() } - func setMode(_ mode: ClawdisCanvasMode) { - self.mode = mode - self.reload() - } - func navigate(to urlString: String) { - self.urlString = urlString - if !urlString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - // `canvas.navigate` is expected to show web content; default to WEB mode. - self.mode = .web - } + let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines) + self.urlString = (trimmed == "/" ? "" : trimmed) self.reload() } func reload() { - switch self.mode { - case .web: - let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) + let trimmed = self.urlString.trimmingCharacters(in: .whitespacesAndNewlines) + if trimmed.isEmpty { + guard let url = Self.canvasScaffoldURL else { return } + self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) + return + } else { guard let url = URL(string: trimmed) else { return } if url.isFileURL { self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) } else { self.webView.load(URLRequest(url: url)) } - case .canvas: - guard let url = Self.canvasScaffoldURL else { return } - self.webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent()) } } - func showA2UI() throws { - guard let url = Self.a2uiIndexURL - else { - throw NSError(domain: "Canvas", code: 10, userInfo: [ - NSLocalizedDescriptionKey: "A2UI resources missing (index.html)", - ]) - } - self.mode = .web - self.urlString = url.absoluteString + func showDefaultCanvas() { + self.urlString = "" self.reload() } @@ -160,10 +144,20 @@ final class ScreenController { withExtension: "html") private static let a2uiIndexURL: URL? = ClawdisKitResources.bundle.url(forResource: "index", withExtension: "html") - fileprivate func isBundledA2UIURL(_ url: URL) -> Bool { + fileprivate func isTrustedCanvasUIURL(_ url: URL) -> Bool { guard url.isFileURL else { return false } - guard let expected = Self.a2uiIndexURL else { return false } - return url.standardizedFileURL == expected.standardizedFileURL + let std = url.standardizedFileURL + if let expected = Self.canvasScaffoldURL, + std == expected.standardizedFileURL + { + return true + } + if let expected = Self.a2uiIndexURL, + std == expected.standardizedFileURL + { + return true + } + return false } } @@ -205,8 +199,8 @@ private final class CanvasA2UIActionMessageHandler: NSObject, WKScriptMessageHan guard message.name == Self.messageName else { return } guard let controller else { return } - // Only accept actions from local bundled CanvasA2UI content (not arbitrary web pages). - guard let url = message.webView?.url, controller.isBundledA2UIURL(url) else { return } + // Only accept actions from local bundled canvas/A2UI content (not arbitrary web pages). + guard let url = message.webView?.url, controller.isTrustedCanvasUIURL(url) else { return } let body: [String: Any] = { if let dict = message.body as? [String: Any] { return dict } diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommandParams.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommandParams.swift index 6d357f7f0..ba2da6a57 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommandParams.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommandParams.swift @@ -1,10 +1,5 @@ import Foundation -public enum ClawdisCanvasMode: String, Codable, Sendable { - case canvas - case web -} - public struct ClawdisCanvasNavigateParams: Codable, Sendable, Equatable { public var url: String @@ -13,14 +8,6 @@ public struct ClawdisCanvasNavigateParams: Codable, Sendable, Equatable { } } -public struct ClawdisCanvasSetModeParams: Codable, Sendable, Equatable { - public var mode: ClawdisCanvasMode - - public init(mode: ClawdisCanvasMode) { - self.mode = mode - } -} - public struct ClawdisCanvasEvalParams: Codable, Sendable, Equatable { public var javaScript: String diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift index 3fe95e7eb..6e2037910 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/CanvasCommands.swift @@ -3,7 +3,6 @@ import Foundation public enum ClawdisCanvasCommand: String, Codable, Sendable { case show = "canvas.show" case hide = "canvas.hide" - case setMode = "canvas.setMode" case navigate = "canvas.navigate" case evalJS = "canvas.eval" case snapshot = "canvas.snapshot" diff --git a/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasScaffold/scaffold.html b/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasScaffold/scaffold.html index 242caa425..ca299986e 100644 --- a/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasScaffold/scaffold.html +++ b/apps/shared/ClawdisKit/Sources/ClawdisKit/Resources/CanvasScaffold/scaffold.html @@ -69,10 +69,13 @@ 100% { transform: translate3d(-10px, 8px, 0) scale(1.03); opacity: 0.43; } } canvas { + position: fixed; + inset: 0; display:block; width:100vw; height:100vh; touch-action: none; + z-index: 1; } #clawdis-status { position: fixed; @@ -80,6 +83,17 @@ display: grid; place-items: center; pointer-events: none; + z-index: 3; + } + #clawdis-a2ui-wrap { + position: fixed; + inset: 0; + display: none; + z-index: 2; + } + #clawdis-a2ui-wrap clawdis-a2ui-host { + display: block; + height: 100%; } #clawdis-status .card { text-align: center; @@ -112,6 +126,9 @@
Waiting for agent
+
+ +
- diff --git a/docs/android/connect.md b/docs/android/connect.md index 93bb55960..65c3b8c5b 100644 --- a/docs/android/connect.md +++ b/docs/android/connect.md @@ -30,7 +30,7 @@ pnpm clawdis gateway --port 18789 --verbose ``` Confirm in logs you see something like: -- `bridge listening on tcp://0.0.0.0:18790 (Iris)` +- `bridge listening on tcp://0.0.0.0:18790 (node)` For tailnet-only setups (recommended for Vienna ⇄ London), bind the bridge to the gateway machine’s Tailscale IP instead: @@ -103,28 +103,22 @@ The Android node’s Chat sheet uses the gateway’s **primary session key** (`m ### Gateway Canvas Host (recommended for web content) -If you want the node to show real HTML/CSS/JS that the agent can edit on disk, enable the Gateway canvas host and point the node at it. +If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point the node at the Gateway canvas host. -1) On the gateway host, enable `canvasHost` in `~/.clawdis/clawdis.json`: +1) Create `~/clawd/canvas/index.html` on the gateway host. -```json5 -{ - canvasHost: { enabled: true, root: "~/clawd/canvas", port: 18793, bind: "lan" } -} -``` - -2) Create `~/clawd/canvas/index.html`. - -3) Navigate the node to it (LAN): +2) Navigate the node to it (LAN): ```bash clawdis nodes invoke --node "" --command canvas.navigate --params '{"url":"http://.local:18793/"}' ``` +Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of `.local`, e.g. `http://:18793/`. + This server injects a live-reload client into HTML and reloads on file changes. Canvas commands (foreground only): -- `canvas.eval`, `canvas.snapshot`, `canvas.navigate` (switches to web mode), `canvas.setMode` (use `"canvas"` to return) +- `canvas.eval`, `canvas.snapshot`, `canvas.navigate` (use `{"url":""}` or `{"url":"/"}` to return to the default canvas/A2UI scaffold) - A2UI: `canvas.a2ui.push`, `canvas.a2ui.reset` (`canvas.a2ui.pushJSONL` legacy alias) Camera commands (foreground only; permission-gated): diff --git a/docs/ios/connect.md b/docs/ios/connect.md index 1834314df..472b69f25 100644 --- a/docs/ios/connect.md +++ b/docs/ios/connect.md @@ -1,23 +1,23 @@ --- -summary: "Runbook: connect/pair the iOS node (Iris) to a Clawdis Gateway and drive its Canvas" +summary: "Runbook: connect/pair the iOS node to a Clawdis Gateway and drive its Canvas" read_when: - Pairing or reconnecting the iOS node - Debugging iOS bridge discovery or auth - Sending screen/canvas commands to iOS --- -# iOS Node Connection Runbook (Iris) +# iOS Node Connection Runbook -This is the practical “how do I connect Iris” guide: +This is the practical “how do I connect the iOS node” guide: **iOS app** ⇄ (Bonjour + TCP bridge) ⇄ **Gateway bridge** ⇄ (loopback WS) ⇄ **Gateway** -The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). Iris talks to the LAN-facing **bridge** (default `tcp://0.0.0.0:18790`) and uses Gateway-owned pairing. +The Gateway WebSocket stays loopback-only (`ws://127.0.0.1:18789`). The iOS node talks to the LAN-facing **bridge** (default `tcp://0.0.0.0:18790`) and uses Gateway-owned pairing. ## Prerequisites - You can run the Gateway on the “master” machine. -- Iris (iOS app) can reach the gateway bridge: +- iOS node app can reach the gateway bridge: - Same LAN with Bonjour/mDNS, **or** - Same Tailscale tailnet using Wide-Area Bonjour / unicast DNS-SD (see below), **or** - Manual bridge host/port (fallback) @@ -32,7 +32,7 @@ pnpm clawdis gateway --port 18789 --verbose ``` Confirm in logs you see something like: -- `bridge listening on tcp://0.0.0.0:18790 (Iris)` +- `bridge listening on tcp://0.0.0.0:18790 (node)` For tailnet-only setups (recommended for Vienna ⇄ London), bind the bridge to the gateway machine’s Tailscale IP instead: @@ -49,7 +49,7 @@ dns-sd -B _clawdis-bridge._tcp local. You should see your gateway advertising `_clawdis-bridge._tcp`. -If browse works, but Iris can’t connect, try resolving one instance: +If browse works, but the iOS node can’t connect, try resolving one instance: ```bash dns-sd -L "" _clawdis-bridge._tcp local. @@ -59,19 +59,19 @@ More debugging notes: `docs/bonjour.md`. ### Tailnet (Vienna ⇄ London) discovery via unicast DNS-SD -If Iris and the gateway are on different networks but connected via Tailscale, multicast mDNS won’t cross the boundary. Use Wide-Area Bonjour / unicast DNS-SD instead: +If the iOS node and the gateway are on different networks but connected via Tailscale, multicast mDNS won’t cross the boundary. Use Wide-Area Bonjour / unicast DNS-SD instead: 1) Set up a DNS-SD zone (example `clawdis.internal.`) on the gateway host and publish `_clawdis-bridge._tcp` records. 2) Configure Tailscale split DNS for `clawdis.internal` pointing at that DNS server. Details and example CoreDNS config: `docs/bonjour.md`. -## 3) Connect from Iris (iOS) +## 3) Connect from the iOS node app -In Iris: +In the iOS node app: - Pick the discovered bridge (or hit refresh). -- If not paired yet, Iris will initiate pairing automatically. -- After the first successful pairing, Iris will auto-reconnect **strictly to the last discovered gateway** on launch (including after reinstall), as long as the iOS Keychain entry is still present. +- If not paired yet, it will initiate pairing automatically. +- After the first successful pairing, it will auto-reconnect **strictly to the last discovered gateway** on launch (including after reinstall), as long as the iOS Keychain entry is still present. ### Connection indicator (always visible) @@ -94,7 +94,7 @@ Approve the request: clawdis nodes approve ``` -After approval, Iris receives/stores the token and reconnects authenticated. +After approval, the iOS node receives/stores the token and reconnects authenticated. Pairing details: `docs/gateway/pairing.md`. @@ -117,26 +117,18 @@ Pairing details: `docs/gateway/pairing.md`. ## 6) Drive the iOS Canvas (draw / snapshot) -Iris runs a WKWebView “Canvas” scaffold which exposes: +The iOS node runs a WKWebView “Canvas” scaffold which exposes: - `window.__clawdis.canvas` - `window.__clawdis.ctx` (2D context) - `window.__clawdis.setStatus(title, subtitle)` ### Gateway Canvas Host (recommended for web content) -If you want Iris to show real HTML/CSS/JS that the agent can edit on disk, enable the Gateway canvas host and point Iris at it. +If you want the node to show real HTML/CSS/JS that the agent can edit on disk, point it at the Gateway canvas host. -1) On the gateway host, enable `canvasHost` in `~/.clawdis/clawdis.json`: +1) Create `~/clawd/canvas/index.html` on the gateway host. -```json5 -{ - canvasHost: { enabled: true, root: "~/clawd/canvas", port: 18793, bind: "lan" } -} -``` - -2) Create `~/clawd/canvas/index.html`. - -3) Navigate Iris to it (LAN): +2) Navigate the node to it (LAN): ```bash clawdis nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url":"http://.local:18793/"}' @@ -144,6 +136,7 @@ clawdis nodes invoke --node "iOS Node" --command canvas.navigate --params '{"url Notes: - The server injects a live-reload client into HTML and reloads on file changes. +- Tailnet (optional): if both devices are on Tailscale, use a MagicDNS name or tailnet IP instead of `.local`, e.g. `http://:18793/`. - iOS may require App Transport Security allowances to load plain `http://` URLs; if it fails to load, prefer HTTPS or adjust the iOS app’s ATS config. ### Draw with `canvas.eval` @@ -165,11 +158,12 @@ The response includes `base64` PNG data (for debugging/verification). ## Common gotchas -- **iOS in background:** all `canvas.*` commands fail fast with `NODE_BACKGROUND_UNAVAILABLE` (bring Iris to foreground). +- **iOS in background:** all `canvas.*` commands fail fast with `NODE_BACKGROUND_UNAVAILABLE` (bring the iOS node app to foreground). +- **Return to default scaffold:** `canvas.navigate` with `{"url":""}` or `{"url":"/"}` returns to the built-in canvas/A2UI scaffold. - **mDNS blocked:** some networks block multicast; use a different LAN or plan a tailnet-capable bridge (see `docs/discovery.md`). - **Wrong node selector:** `--node` can be the node id (UUID), display name (e.g. `iOS Node`), IP, or an unambiguous prefix. If it’s ambiguous, the CLI will tell you. -- **Stale pairing / Keychain cleared:** if the pairing token is missing (or iOS Keychain was wiped), Iris must pair again; approve a new pending request. -- **App reinstall but no reconnect:** Iris restores `instanceId` + last bridge preference from Keychain; if it still comes up “unpaired”, verify Keychain persistence on your device/simulator and re-pair once. +- **Stale pairing / Keychain cleared:** if the pairing token is missing (or iOS Keychain was wiped), the node must pair again; approve a new pending request. +- **App reinstall but no reconnect:** the node restores `instanceId` + last bridge preference from Keychain; if it still comes up “unpaired”, verify Keychain persistence on your device/simulator and re-pair once. ## Related docs diff --git a/docs/ios/spec.md b/docs/ios/spec.md index 2d0a087b4..e99976199 100644 --- a/docs/ios/spec.md +++ b/docs/ios/spec.md @@ -120,10 +120,9 @@ Add to `src/gateway/protocol/schema.ts` (and regenerate Swift models): ### Node command set (canvas) These are values for `node.invoke.command`: - `canvas.show` / `canvas.hide` -- `canvas.navigate` with `{ url }` (Canvas URL or https URL; switches mode to `"web"`) +- `canvas.navigate` with `{ url }` (loads a URL; use `""` or `"/"` to return to the default canvas/A2UI scaffold) - `canvas.eval` with `{ javaScript }` - `canvas.snapshot` with `{ maxWidth?, quality?, format? }` -- `canvas.setMode` with `{ mode: "canvas" | "web" }` (use `"canvas"` to return to the scaffold) - A2UI (mobile + macOS canvas): - `canvas.a2ui.push` with `{ messages: [...] }` (A2UI v0.8 server→client messages) - `canvas.a2ui.pushJSONL` with `{ jsonl: "..." }` (legacy alias)