diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 9b37aa40e9f5..eb0bfbdb8e08 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -20899,7 +20899,7 @@ }, { "kind": "ui-modifier", - "line": 155, + "line": 156, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Settings", "surface": "apple", @@ -20907,7 +20907,7 @@ }, { "kind": "ui-modifier", - "line": 260, + "line": 261, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Scan QR Code", "surface": "apple", @@ -20915,7 +20915,7 @@ }, { "kind": "ui-modifier", - "line": 282, + "line": 283, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Reset Onboarding?", "surface": "apple", @@ -20923,7 +20923,7 @@ }, { "kind": "ui-call", - "line": 286, + "line": 287, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Reset", "surface": "apple", @@ -20931,7 +20931,7 @@ }, { "kind": "ui-call", - "line": 294, + "line": 295, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "This disconnects, clears saved gateway credentials, and reopens onboarding.", "surface": "apple", @@ -20939,7 +20939,7 @@ }, { "kind": "ui-modifier", - "line": 298, + "line": 299, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "QR Scanner Unavailable", "surface": "apple", @@ -20947,7 +20947,7 @@ }, { "kind": "ui-call", - "line": 307, + "line": 308, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "OK", "surface": "apple", @@ -20955,7 +20955,7 @@ }, { "kind": "ui-localized-call", - "line": 316, + "line": 317, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Forget %@?", "surface": "apple", @@ -20963,7 +20963,7 @@ }, { "kind": "ui-localized-call", - "line": 317, + "line": 318, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "gateway", "surface": "apple", @@ -20971,7 +20971,7 @@ }, { "kind": "ui-call", - "line": 330, + "line": 331, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Forget Gateway", "surface": "apple", @@ -20979,7 +20979,7 @@ }, { "kind": "ui-call", - "line": 336, + "line": 337, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Cancel", "surface": "apple", @@ -20987,7 +20987,7 @@ }, { "kind": "ui-localized-call", - "line": 344, + "line": 345, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "This removes saved credentials, device access, TLS trust, and cached chats for this gateway.", "surface": "apple", @@ -20995,7 +20995,7 @@ }, { "kind": "ui-call", - "line": 398, + "line": 399, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Enable OpenClaw Hosted Push Relay?", "surface": "apple", @@ -21003,7 +21003,7 @@ }, { "kind": "ui-call", - "line": 412, + "line": 413, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Continue", "surface": "apple", @@ -21011,7 +21011,7 @@ }, { "kind": "ui-call", - "line": 420, + "line": 421, "path": "apps/ios/Sources/Design/SettingsProTab.swift", "source": "Not Now", "surface": "apple", @@ -21285,13 +21285,21 @@ "kind": "ui-localized-call", "line": 936, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "OpenClaw", + "surface": "apple", + "id": "native.apple.2d878afa870cd7cc" + }, + { + "kind": "ui-localized-call", + "line": 937, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Apple Watch", "surface": "apple", "id": "native.apple.b99af99847ac7823" }, { "kind": "ui-localized-call", - "line": 937, + "line": 938, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Approvals", "surface": "apple", @@ -21299,7 +21307,7 @@ }, { "kind": "ui-localized-call", - "line": 938, + "line": 939, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Permissions", "surface": "apple", @@ -21307,7 +21315,7 @@ }, { "kind": "ui-localized-call", - "line": 939, + "line": 940, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Channels", "surface": "apple", @@ -21315,7 +21323,7 @@ }, { "kind": "ui-localized-call", - "line": 940, + "line": 941, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Skills", "surface": "apple", @@ -21323,7 +21331,7 @@ }, { "kind": "ui-localized-call", - "line": 941, + "line": 942, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Voice & Talk", "surface": "apple", @@ -21331,7 +21339,7 @@ }, { "kind": "ui-localized-call", - "line": 942, + "line": 943, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Diagnostics", "surface": "apple", @@ -21339,7 +21347,7 @@ }, { "kind": "ui-localized-call", - "line": 943, + "line": 944, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Privacy", "surface": "apple", @@ -21347,7 +21355,7 @@ }, { "kind": "ui-localized-call", - "line": 945, + "line": 946, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Licenses", "surface": "apple", @@ -21355,7 +21363,7 @@ }, { "kind": "ui-localized-call", - "line": 946, + "line": 947, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "About", "surface": "apple", @@ -21363,7 +21371,7 @@ }, { "kind": "ui-localized-call", - "line": 953, + "line": 954, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Preparing one-time setup…", "surface": "apple", @@ -21371,7 +21379,7 @@ }, { "kind": "ui-localized-call", - "line": 958, + "line": 959, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup sent. Open OpenClaw on the watch to connect.", "surface": "apple", @@ -21379,7 +21387,7 @@ }, { "kind": "ui-localized-call", - "line": 960, + "line": 961, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup queued for the watch. Open OpenClaw before the code expires.", "surface": "apple", @@ -21387,7 +21395,7 @@ }, { "kind": "ui-localized-call", - "line": 1050, + "line": 1051, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This gateway is on your tailnet. Turn on Tailscale on this device, then tap Connect.", "surface": "apple", @@ -21395,7 +21403,7 @@ }, { "kind": "ui-localized-call", - "line": 1057, + "line": 1058, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Pairing required. Run /pair approve in your OpenClaw chat, then connect again.", "surface": "apple", @@ -21403,7 +21411,7 @@ }, { "kind": "ui-localized-call", - "line": 1060, + "line": 1061, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Secure handshake failed. Check Tailscale, then connect again.", "surface": "apple", @@ -21411,7 +21419,7 @@ }, { "kind": "ui-localized-call", - "line": 1069, + "line": 1070, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connection timed out. Make sure Tailscale is connected, then try again.", "surface": "apple", @@ -21419,7 +21427,7 @@ }, { "kind": "ui-localized-call", - "line": 1073, + "line": 1074, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connected, but some controls are restricted for nodes. This is expected.", "surface": "apple", @@ -21427,7 +21435,7 @@ }, { "kind": "ui-localized-call", - "line": 1080, + "line": 1081, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Setup code applied. Connecting...", "surface": "apple", @@ -21435,7 +21443,7 @@ }, { "kind": "ui-localized-call", - "line": 1081, + "line": 1082, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Checking gateway reachability...", "surface": "apple", @@ -21443,7 +21451,7 @@ }, { "kind": "ui-localized-call", - "line": 1082, + "line": 1083, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "QR loaded. Connecting to %@:%@...", "surface": "apple", @@ -21451,7 +21459,7 @@ }, { "kind": "ui-localized-call", - "line": 1145, + "line": 1146, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not loaded", "surface": "apple", @@ -21459,7 +21467,7 @@ }, { "kind": "ui-localized-call", - "line": 1148, + "line": 1149, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Configured", "surface": "apple", @@ -21467,7 +21475,7 @@ }, { "kind": "ui-localized-call", - "line": 1149, + "line": 1150, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not configured", "surface": "apple", @@ -21475,7 +21483,7 @@ }, { "kind": "ui-localized-call", - "line": 1156, + "line": 1157, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Not active", "surface": "apple", @@ -21483,7 +21491,7 @@ }, { "kind": "ui-localized-call", - "line": 1192, + "line": 1193, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Apple Review demo mode", "surface": "apple", @@ -21491,7 +21499,7 @@ }, { "kind": "ui-localized-call", - "line": 1195, + "line": 1196, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connected", "surface": "apple", @@ -21499,7 +21507,7 @@ }, { "kind": "ui-localized-call", - "line": 1201, + "line": 1202, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "offline", "surface": "apple", @@ -21507,7 +21515,7 @@ }, { "kind": "ui-localized-call", - "line": 1201, + "line": 1202, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "online", "surface": "apple", @@ -21515,7 +21523,7 @@ }, { "kind": "ui-localized-call", - "line": 1219, + "line": 1220, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Live gateway requests are disabled in demo mode.", "surface": "apple", @@ -21523,7 +21531,7 @@ }, { "kind": "ui-localized-call", - "line": 1223, + "line": 1224, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Foreground approvals still appear while OpenClaw is connected.", "surface": "apple", @@ -21531,7 +21539,7 @@ }, { "kind": "ui-localized-call", - "line": 1226, + "line": 1227, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Gateway requests will appear here.", "surface": "apple", @@ -21539,7 +21547,7 @@ }, { "kind": "ui-localized-call", - "line": 1227, + "line": 1228, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Connect to the gateway.", "surface": "apple", @@ -21547,7 +21555,7 @@ }, { "kind": "ui-localized-call", - "line": 1231, + "line": 1232, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Demo mode only", "surface": "apple", @@ -21555,7 +21563,7 @@ }, { "kind": "ui-localized-call", - "line": 1238, + "line": 1239, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "loaded", "surface": "apple", @@ -21563,7 +21571,7 @@ }, { "kind": "ui-localized-call", - "line": 1239, + "line": 1240, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "missing", "surface": "apple", @@ -21571,7 +21579,7 @@ }, { "kind": "ui-localized-call", - "line": 1248, + "line": 1249, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Waiting for gateway", "surface": "apple", @@ -21579,7 +21587,7 @@ }, { "kind": "ui-localized-call", - "line": 1265, + "line": 1266, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "1 waiting", "surface": "apple", @@ -21587,7 +21595,7 @@ }, { "kind": "ui-localized-call", - "line": 1268, + "line": 1269, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "%@ waiting", "surface": "apple", @@ -21595,7 +21603,7 @@ }, { "kind": "ui-localized-call", - "line": 1279, + "line": 1280, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Review gateway action", "surface": "apple", @@ -21603,7 +21611,7 @@ }, { "kind": "ui-localized-call", - "line": 1281, + "line": 1282, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Agent: %@", "surface": "apple", @@ -21611,7 +21619,7 @@ }, { "kind": "ui-localized-call", - "line": 1290, + "line": 1291, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Resolving", "surface": "apple", @@ -21619,7 +21627,7 @@ }, { "kind": "ui-localized-call", - "line": 1291, + "line": 1292, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "High", "surface": "apple", @@ -21627,7 +21635,7 @@ }, { "kind": "ui-localized-call", - "line": 1297, + "line": 1298, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Permission can be saved", "surface": "apple", @@ -21635,7 +21643,7 @@ }, { "kind": "ui-localized-call", - "line": 1298, + "line": 1299, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "One-time approval", "surface": "apple", @@ -21643,7 +21651,7 @@ }, { "kind": "ui-localized-call", - "line": 1301, + "line": 1302, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Medium", "surface": "apple", @@ -21651,7 +21659,7 @@ }, { "kind": "ui-localized-call", - "line": 1302, + "line": 1303, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Review", "surface": "apple", @@ -21659,7 +21667,7 @@ }, { "kind": "ui-localized-call", - "line": 1308, + "line": 1309, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Talk + Wake", "surface": "apple", @@ -21667,7 +21675,7 @@ }, { "kind": "ui-localized-call", - "line": 1309, + "line": 1310, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Talk on", "surface": "apple", @@ -21675,7 +21683,7 @@ }, { "kind": "ui-localized-call", - "line": 1310, + "line": 1311, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Wake on", "surface": "apple", @@ -21683,7 +21691,7 @@ }, { "kind": "ui-localized-call", - "line": 1311, + "line": 1312, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Off", "surface": "apple", @@ -21691,7 +21699,7 @@ }, { "kind": "ui-localized-call", - "line": 1315, + "line": 1316, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "demo", "surface": "apple", @@ -21699,7 +21707,7 @@ }, { "kind": "ui-localized-call", - "line": 1316, + "line": 1317, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "ready", "surface": "apple", @@ -21707,7 +21715,7 @@ }, { "kind": "ui-localized-call", - "line": 1317, + "line": 1318, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "check", "surface": "apple", @@ -21715,7 +21723,7 @@ }, { "kind": "ui-localized-call", - "line": 1318, + "line": 1319, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "partial", "surface": "apple", @@ -21723,7 +21731,7 @@ }, { "kind": "ui-localized-call", - "line": 1322, + "line": 1323, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "pending", "surface": "apple", @@ -21731,7 +21739,7 @@ }, { "kind": "ui-localized-call", - "line": 1324, + "line": 1325, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "pass", "surface": "apple", @@ -21739,7 +21747,7 @@ }, { "kind": "ui-localized-call", - "line": 1335, + "line": 1336, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Requesting iOS location permission…", "surface": "apple", @@ -21747,7 +21755,7 @@ }, { "kind": "ui-localized-call", - "line": 1401, + "line": 1402, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This build uses OpenClaw's hosted push relay at %@ for notification delivery data.", "surface": "apple", @@ -21755,7 +21763,7 @@ }, { "kind": "ui-localized-call", - "line": 1405, + "line": 1406, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "This build is not configured to use OpenClaw's hosted push relay.", "surface": "apple", @@ -21763,7 +21771,7 @@ }, { "kind": "ui-localized-call", - "line": 1410, + "line": 1411, "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", "source": "Enabling this sends delivery data through OpenClaw's hosted push relay.", "surface": "apple", @@ -21827,7 +21835,15 @@ }, { "kind": "ui-named-argument", - "line": 170, + "line": 164, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "OpenClaw", + "surface": "apple", + "id": "native.apple.fc49f9b63e160581" + }, + { + "kind": "ui-named-argument", + "line": 176, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Permissions", "surface": "apple", @@ -21835,7 +21851,7 @@ }, { "kind": "ui-named-argument", - "line": 175, + "line": 181, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Channels", "surface": "apple", @@ -21843,7 +21859,7 @@ }, { "kind": "ui-named-argument", - "line": 180, + "line": 186, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Skills", "surface": "apple", @@ -21851,7 +21867,7 @@ }, { "kind": "ui-named-argument", - "line": 194, + "line": 200, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Diagnostics", "surface": "apple", @@ -21859,7 +21875,7 @@ }, { "kind": "ui-named-argument", - "line": 199, + "line": 205, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Privacy", "surface": "apple", @@ -21867,7 +21883,7 @@ }, { "kind": "ui-named-argument", - "line": 209, + "line": 215, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "About", "surface": "apple", @@ -21875,7 +21891,7 @@ }, { "kind": "ui-named-argument", - "line": 221, + "line": 227, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Licenses", "surface": "apple", @@ -21883,7 +21899,7 @@ }, { "kind": "ui-call", - "line": 346, + "line": 356, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Address", "surface": "apple", @@ -21891,7 +21907,7 @@ }, { "kind": "ui-call", - "line": 347, + "line": 357, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Server", "surface": "apple", @@ -21899,7 +21915,7 @@ }, { "kind": "ui-call", - "line": 348, + "line": 358, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovered", "surface": "apple", @@ -21907,7 +21923,7 @@ }, { "kind": "ui-call", - "line": 354, + "line": 364, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Agents", "surface": "apple", @@ -21915,7 +21931,7 @@ }, { "kind": "ui-call", - "line": 357, + "line": 367, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Access", "surface": "apple", @@ -21923,7 +21939,7 @@ }, { "kind": "conditional-branch", - "line": 361, + "line": 371, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Full", "surface": "apple", @@ -21931,7 +21947,7 @@ }, { "kind": "conditional-branch", - "line": 361, + "line": 371, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Limited", "surface": "apple", @@ -21939,7 +21955,7 @@ }, { "kind": "ui-call", - "line": 368, + "line": 378, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Upgrade access", "surface": "apple", @@ -21947,7 +21963,7 @@ }, { "kind": "ui-call", - "line": 370, + "line": 380, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "This phone has limited Gateway access.", "surface": "apple", @@ -21955,7 +21971,7 @@ }, { "kind": "ui-call", - "line": 372, + "line": 382, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Use a secure wss:// or Tailscale Serve Gateway, then scan a full-access setup code from the Control UI or openclaw qr and reconnect to enable settings and upgrades.", "surface": "apple", @@ -21963,7 +21979,7 @@ }, { "kind": "ui-call", - "line": 381, + "line": 391, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Scan Full-Access Code", "surface": "apple", @@ -21971,7 +21987,7 @@ }, { "kind": "ui-named-argument", - "line": 407, + "line": 417, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway", "surface": "apple", @@ -21979,7 +21995,7 @@ }, { "kind": "conditional-branch", - "line": 411, + "line": 421, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Scan QR to Pair", "surface": "apple", @@ -21987,7 +22003,7 @@ }, { "kind": "ui-modifier", - "line": 442, + "line": 452, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Switch Gateway", "surface": "apple", @@ -21995,7 +22011,7 @@ }, { "kind": "ui-named-argument", - "line": 449, + "line": 459, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Approvals", "surface": "apple", @@ -22003,7 +22019,7 @@ }, { "kind": "ui-localized-call", - "line": 451, + "line": 461, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Out-of-app approval alerts need notification permission.", "surface": "apple", @@ -22011,7 +22027,7 @@ }, { "kind": "ui-localized-call", - "line": 453, + "line": 463, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "No gateway actions are waiting for review.", "surface": "apple", @@ -22019,7 +22035,7 @@ }, { "kind": "ui-localized-call", - "line": 454, + "line": 464, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Review pending gateway actions.", "surface": "apple", @@ -22027,7 +22043,7 @@ }, { "kind": "ui-localized-call", - "line": 456, + "line": 466, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Alerts Off", "surface": "apple", @@ -22035,7 +22051,7 @@ }, { "kind": "ui-localized-call", - "line": 458, + "line": 468, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "clear", "surface": "apple", @@ -22043,7 +22059,7 @@ }, { "kind": "ui-named-argument", - "line": 476, + "line": 486, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Apple Watch", "surface": "apple", @@ -22051,7 +22067,7 @@ }, { "kind": "ui-localized-call", - "line": 479, + "line": 489, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Relay remains available; direct mode adds an independent Gateway node.", "surface": "apple", @@ -22059,7 +22075,7 @@ }, { "kind": "ui-localized-call", - "line": 481, + "line": 491, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Install the OpenClaw watch app before enabling direct mode.", "surface": "apple", @@ -22067,7 +22083,7 @@ }, { "kind": "ui-localized-call", - "line": 484, + "line": 494, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reachable", "surface": "apple", @@ -22075,7 +22091,7 @@ }, { "kind": "ui-localized-call", - "line": 486, + "line": 496, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Installed", "surface": "apple", @@ -22083,7 +22099,7 @@ }, { "kind": "ui-localized-call", - "line": 487, + "line": 497, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Unavailable", "surface": "apple", @@ -22091,7 +22107,7 @@ }, { "kind": "ui-call", - "line": 494, + "line": 504, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Enable Direct Gateway Connection", "surface": "apple", @@ -22099,7 +22115,7 @@ }, { "kind": "ui-call", - "line": 510, + "line": 520, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "The watch receives a one-time pairing code and stores its own device token. A reachable secure Gateway URL is required away from the iPhone.", "surface": "apple", @@ -22107,7 +22123,7 @@ }, { "kind": "ui-call", - "line": 518, + "line": 528, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Direct node features", "surface": "apple", @@ -22115,7 +22131,7 @@ }, { "kind": "ui-call", - "line": 528, + "line": 538, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Notifications are off", "surface": "apple", @@ -22123,7 +22139,7 @@ }, { "kind": "ui-call", - "line": 530, + "line": 540, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Enable Notifications to receive approval alerts while OpenClaw is not open.", "surface": "apple", @@ -22131,7 +22147,7 @@ }, { "kind": "ui-call", - "line": 539, + "line": 549, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Open Notifications", "surface": "apple", @@ -22139,7 +22155,7 @@ }, { "kind": "ui-call", - "line": 549, + "line": 559, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Pending approvals", "surface": "apple", @@ -22147,7 +22163,7 @@ }, { "kind": "ui-modifier", - "line": 566, + "line": 576, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Review exec approval", "surface": "apple", @@ -22155,7 +22171,7 @@ }, { "kind": "ui-call", - "line": 573, + "line": 583, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reviewing", "surface": "apple", @@ -22163,7 +22179,7 @@ }, { "kind": "ui-call", - "line": 607, + "line": 617, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Allow Once", "surface": "apple", @@ -22171,7 +22187,7 @@ }, { "kind": "ui-call", - "line": 616, + "line": 626, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Allow Always", "surface": "apple", @@ -22179,7 +22195,7 @@ }, { "kind": "ui-call", - "line": 625, + "line": 635, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Deny", "surface": "apple", @@ -22187,7 +22203,7 @@ }, { "kind": "ui-call", - "line": 636, + "line": 646, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Dismiss", "surface": "apple", @@ -22195,7 +22211,7 @@ }, { "kind": "ui-call", - "line": 646, + "line": 656, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "No approvals waiting", "surface": "apple", @@ -22203,7 +22219,7 @@ }, { "kind": "ui-named-argument", - "line": 676, + "line": 686, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Camera", "surface": "apple", @@ -22211,7 +22227,7 @@ }, { "kind": "ui-named-argument", - "line": 682, + "line": 692, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Keep Awake", "surface": "apple", @@ -22219,7 +22235,7 @@ }, { "kind": "ui-named-argument", - "line": 694, + "line": 704, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice & Talk", "surface": "apple", @@ -22227,7 +22243,7 @@ }, { "kind": "ui-named-argument", - "line": 709, + "line": 719, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Health Check", "surface": "apple", @@ -22235,7 +22251,7 @@ }, { "kind": "ui-named-argument", - "line": 710, + "line": 720, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Run app, permission, and gateway-adjacent checks without editing setup.", "surface": "apple", @@ -22243,7 +22259,7 @@ }, { "kind": "ui-call", - "line": 718, + "line": 728, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Run Diagnostics", "surface": "apple", @@ -22251,7 +22267,7 @@ }, { "kind": "ui-call", - "line": 728, + "line": 738, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Platform", "surface": "apple", @@ -22259,7 +22275,7 @@ }, { "kind": "ui-call", - "line": 731, + "line": 741, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "App", "surface": "apple", @@ -22267,7 +22283,7 @@ }, { "kind": "ui-call", - "line": 734, + "line": 744, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Model", "surface": "apple", @@ -22275,7 +22291,7 @@ }, { "kind": "ui-named-argument", - "line": 746, + "line": 756, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Camera Access", "surface": "apple", @@ -22283,7 +22299,7 @@ }, { "kind": "ui-named-argument", - "line": 752, + "line": 762, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Background Listening", "surface": "apple", @@ -22291,7 +22307,7 @@ }, { "kind": "ui-call", - "line": 778, + "line": 788, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Notifications", "surface": "apple", @@ -22299,7 +22315,7 @@ }, { "kind": "ui-modifier", - "line": 787, + "line": 797, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Turns OpenClaw notification delivery on or off", "surface": "apple", @@ -22307,7 +22323,7 @@ }, { "kind": "ui-named-argument", - "line": 807, + "line": 817, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reconnect", "surface": "apple", @@ -22315,7 +22331,7 @@ }, { "kind": "ui-named-argument", - "line": 817, + "line": 827, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Diagnose", "surface": "apple", @@ -22323,7 +22339,7 @@ }, { "kind": "ui-call", - "line": 833, + "line": 843, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "License files are not available in this build.", "surface": "apple", @@ -22331,7 +22347,7 @@ }, { "kind": "ui-call", - "line": 850, + "line": 860, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "OpenClaw appreciates its partners in the open-source community.", "surface": "apple", @@ -22339,15 +22355,7 @@ }, { "kind": "ui-call", - "line": 890, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "OpenClaw", - "surface": "apple", - "id": "native.apple.fc49f9b63e160581" - }, - { - "kind": "ui-call", - "line": 892, + "line": 902, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Personal AI on your devices", "surface": "apple", @@ -22355,7 +22363,7 @@ }, { "kind": "ui-call", - "line": 909, + "line": 919, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "iOS", "surface": "apple", @@ -22363,7 +22371,7 @@ }, { "kind": "ui-named-argument", - "line": 916, + "line": 926, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Website", "surface": "apple", @@ -22371,7 +22379,7 @@ }, { "kind": "ui-named-argument", - "line": 921, + "line": 931, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Docs", "surface": "apple", @@ -22379,7 +22387,7 @@ }, { "kind": "ui-named-argument", - "line": 926, + "line": 936, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "GitHub", "surface": "apple", @@ -22387,7 +22395,7 @@ }, { "kind": "ui-named-argument", - "line": 931, + "line": 941, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discord", "surface": "apple", @@ -22395,7 +22403,7 @@ }, { "kind": "ui-call", - "line": 936, + "line": 946, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "© 2026 OpenClaw Foundation — MIT License.", "surface": "apple", @@ -22403,7 +22411,7 @@ }, { "kind": "ui-call", - "line": 942, + "line": 952, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Title", "surface": "apple", @@ -22411,7 +22419,7 @@ }, { "kind": "ui-call", - "line": 982, + "line": 992, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Location", "surface": "apple", @@ -22419,7 +22427,7 @@ }, { "kind": "ui-modifier", - "line": 1000, + "line": 1010, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Location Sharing", "surface": "apple", @@ -22427,7 +22435,7 @@ }, { "kind": "ui-call", - "line": 1013, + "line": 1023, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "While Using the App", "surface": "apple", @@ -22435,7 +22443,7 @@ }, { "kind": "ui-call", - "line": 1019, + "line": 1029, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Always", "surface": "apple", @@ -22443,7 +22451,7 @@ }, { "kind": "ui-call", - "line": 1024, + "line": 1034, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Access Level", "surface": "apple", @@ -22451,7 +22459,7 @@ }, { "kind": "ui-modifier", - "line": 1046, + "line": 1056, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Chooses While Using the App or Always", "surface": "apple", @@ -22459,7 +22467,7 @@ }, { "kind": "ui-call", - "line": 1066, + "line": 1076, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Default Agent", "surface": "apple", @@ -22467,7 +22475,7 @@ }, { "kind": "ui-call", - "line": 1067, + "line": 1077, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Default", "surface": "apple", @@ -22475,7 +22483,7 @@ }, { "kind": "ui-call", - "line": 1080, + "line": 1090, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Used for new Chat and Talk sessions.", "surface": "apple", @@ -22483,7 +22491,7 @@ }, { "kind": "ui-named-argument", - "line": 1090, + "line": 1100, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Scan QR", "surface": "apple", @@ -22491,7 +22499,7 @@ }, { "kind": "ui-call", - "line": 1098, + "line": 1108, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Paste setup code", "surface": "apple", @@ -22499,7 +22507,7 @@ }, { "kind": "ui-named-argument", - "line": 1104, + "line": 1114, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Connect", "surface": "apple", @@ -22507,7 +22515,7 @@ }, { "kind": "ui-call", - "line": 1113, + "line": 1123, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "No gateways found yet. Use manual setup if Bonjour is blocked.", "surface": "apple", @@ -22515,7 +22523,7 @@ }, { "kind": "ui-call", - "line": 1122, + "line": 1132, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Add Gateway", "surface": "apple", @@ -22523,7 +22531,7 @@ }, { "kind": "ui-call", - "line": 1137, + "line": 1147, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Pair a gateway to make it available here.", "surface": "apple", @@ -22531,7 +22539,7 @@ }, { "kind": "ui-call", - "line": 1146, + "line": 1156, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Paired Gateways", "surface": "apple", @@ -22539,7 +22547,7 @@ }, { "kind": "ui-call", - "line": 1149, + "line": 1159, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Keep multiple gateways connected and switch which one is in focus.", "surface": "apple", @@ -22547,7 +22555,7 @@ }, { "kind": "ui-modifier", - "line": 1186, + "line": 1196, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Focused Gateway", "surface": "apple", @@ -22555,7 +22563,7 @@ }, { "kind": "conditional-branch", - "line": 1201, + "line": 1211, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Disconnect Gateway", "surface": "apple", @@ -22563,7 +22571,7 @@ }, { "kind": "conditional-branch", - "line": 1201, + "line": 1211, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Keep Gateway Connected", "surface": "apple", @@ -22571,7 +22579,7 @@ }, { "kind": "ui-call", - "line": 1210, + "line": 1220, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Forget", "surface": "apple", @@ -22579,7 +22587,7 @@ }, { "kind": "ui-call", - "line": 1222, + "line": 1232, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Forget Gateway", "surface": "apple", @@ -22587,7 +22595,7 @@ }, { "kind": "ui-call", - "line": 1275, + "line": 1285, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Manual Gateway", "surface": "apple", @@ -22595,7 +22603,7 @@ }, { "kind": "ui-call", - "line": 1276, + "line": 1286, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Use Manual Gateway", "surface": "apple", @@ -22603,7 +22611,7 @@ }, { "kind": "ui-call", - "line": 1277, + "line": 1287, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Host", "surface": "apple", @@ -22611,7 +22619,7 @@ }, { "kind": "ui-call", - "line": 1281, + "line": 1291, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Port", "surface": "apple", @@ -22619,7 +22627,7 @@ }, { "kind": "ui-call", - "line": 1285, + "line": 1295, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Unencrypted", "surface": "apple", @@ -22627,7 +22635,7 @@ }, { "kind": "ui-call", - "line": 1288, + "line": 1298, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Secure (TLS)", "surface": "apple", @@ -22635,7 +22643,7 @@ }, { "kind": "ui-call", - "line": 1292, + "line": 1302, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Connection security", "surface": "apple", @@ -22643,7 +22651,7 @@ }, { "kind": "ui-named-argument", - "line": 1303, + "line": 1313, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Connect Manual", "surface": "apple", @@ -22651,7 +22659,7 @@ }, { "kind": "ui-call", - "line": 1333, + "line": 1343, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Auto-connect on launch", "surface": "apple", @@ -22659,7 +22667,7 @@ }, { "kind": "ui-call", - "line": 1334, + "line": 1344, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Auth Token", "surface": "apple", @@ -22667,7 +22675,7 @@ }, { "kind": "ui-call", - "line": 1335, + "line": 1345, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Password", "surface": "apple", @@ -22675,7 +22683,7 @@ }, { "kind": "ui-call", - "line": 1340, + "line": 1350, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Custom Headers", "surface": "apple", @@ -22683,7 +22691,7 @@ }, { "kind": "ui-call", - "line": 1347, + "line": 1357, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reset Onboarding", "surface": "apple", @@ -22691,7 +22699,7 @@ }, { "kind": "ui-call", - "line": 1377, + "line": 1387, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice Wake", "surface": "apple", @@ -22699,7 +22707,7 @@ }, { "kind": "ui-call", - "line": 1380, + "line": 1390, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Talk Mode", "surface": "apple", @@ -22707,7 +22715,7 @@ }, { "kind": "ui-call", - "line": 1388, + "line": 1398, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Speech Language", "surface": "apple", @@ -22715,7 +22723,7 @@ }, { "kind": "ui-call", - "line": 1395, + "line": 1405, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Speakerphone", "surface": "apple", @@ -22723,7 +22731,7 @@ }, { "kind": "ui-call", - "line": 1399, + "line": 1409, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Wake Words", "surface": "apple", @@ -22731,7 +22739,7 @@ }, { "kind": "ui-call", - "line": 1421, + "line": 1431, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice", "surface": "apple", @@ -22739,7 +22747,7 @@ }, { "kind": "ui-call", - "line": 1422, + "line": 1432, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Provider", "surface": "apple", @@ -22747,7 +22755,7 @@ }, { "kind": "ui-call", - "line": 1429, + "line": 1439, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Realtime Voice", "surface": "apple", @@ -22755,7 +22763,7 @@ }, { "kind": "ui-call", - "line": 1430, + "line": 1440, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway Default", "surface": "apple", @@ -22763,7 +22771,7 @@ }, { "kind": "ui-call", - "line": 1437, + "line": 1447, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice Mode", "surface": "apple", @@ -22771,7 +22779,7 @@ }, { "kind": "ui-call", - "line": 1440, + "line": 1450, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Active Voice", "surface": "apple", @@ -22779,7 +22787,7 @@ }, { "kind": "ui-call", - "line": 1444, + "line": 1454, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Last Voice Issue", "surface": "apple", @@ -22787,7 +22795,7 @@ }, { "kind": "ui-call", - "line": 1446, + "line": 1456, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Transport", "surface": "apple", @@ -22795,7 +22803,7 @@ }, { "kind": "ui-call", - "line": 1449, + "line": 1459, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "API Key", "surface": "apple", @@ -22803,7 +22811,7 @@ }, { "kind": "ui-call", - "line": 1456, + "line": 1466, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Show Talk Control", "surface": "apple", @@ -22811,7 +22819,7 @@ }, { "kind": "ui-call", - "line": 1457, + "line": 1467, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Default Share Instruction", "surface": "apple", @@ -22819,7 +22827,7 @@ }, { "kind": "ui-call", - "line": 1464, + "line": 1474, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Run Share Self-Test", "surface": "apple", @@ -22827,7 +22835,7 @@ }, { "kind": "ui-call", - "line": 1483, + "line": 1493, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Apple Health", "surface": "apple", @@ -22835,7 +22843,7 @@ }, { "kind": "ui-call", - "line": 1491, + "line": 1501, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovery Debug Logs", "surface": "apple", @@ -22843,7 +22851,7 @@ }, { "kind": "ui-call", - "line": 1494, + "line": 1504, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Debug Screen Status", "surface": "apple", @@ -22851,7 +22859,7 @@ }, { "kind": "ui-call", - "line": 1498, + "line": 1508, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovery Logs", "surface": "apple", @@ -22859,7 +22867,7 @@ }, { "kind": "ui-call", - "line": 1506, + "line": 1516, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Device", "surface": "apple", @@ -22867,7 +22875,7 @@ }, { "kind": "ui-call", - "line": 1507, + "line": 1517, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Device Name", "surface": "apple", @@ -22875,7 +22883,7 @@ }, { "kind": "ui-call", - "line": 1509, + "line": 1519, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Instance ID", "surface": "apple", @@ -22883,7 +22891,7 @@ }, { "kind": "ui-localized-call", - "line": 1533, + "line": 1543, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "On", "surface": "apple", @@ -22891,7 +22899,7 @@ }, { "kind": "ui-localized-call", - "line": 1534, + "line": 1544, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Off", "surface": "apple", @@ -22899,7 +22907,7 @@ }, { "kind": "ui-call", - "line": 99, + "line": 100, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Copy full commit hash", "surface": "apple", @@ -22907,7 +22915,7 @@ }, { "kind": "ui-call", - "line": 106, + "line": 107, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Copy build info", "surface": "apple", @@ -22915,7 +22923,7 @@ }, { "kind": "ui-call", - "line": 116, + "line": 117, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Copy Commit", "surface": "apple", @@ -22923,7 +22931,7 @@ }, { "kind": "ui-call", - "line": 127, + "line": 128, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Copy Build Info", "surface": "apple", @@ -22931,7 +22939,7 @@ }, { "kind": "ui-call", - "line": 168, + "line": 169, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Unavailable", "surface": "apple", @@ -22939,7 +22947,7 @@ }, { "kind": "ui-localized-call", - "line": 188, + "line": 189, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Version %1$@, commit %2$@, built %3$@, timestamp %4$@", "surface": "apple", @@ -22947,7 +22955,7 @@ }, { "kind": "ui-localized-call", - "line": 197, + "line": 198, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Version %1$@, commit %2$@, build date unavailable", "surface": "apple", @@ -22955,7 +22963,7 @@ }, { "kind": "ui-localized-call", - "line": 204, + "line": 205, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Version %1$@, commit unavailable, built %2$@, timestamp %3$@", "surface": "apple", @@ -22963,7 +22971,7 @@ }, { "kind": "ui-localized-call", - "line": 211, + "line": 212, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Version %@, commit unavailable, build date unavailable", "surface": "apple", @@ -22971,7 +22979,7 @@ }, { "kind": "ui-localized-call", - "line": 307, + "line": 308, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Checking", "surface": "apple", @@ -22979,7 +22987,7 @@ }, { "kind": "ui-localized-call", - "line": 308, + "line": 309, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Enabled", "surface": "apple", @@ -22987,7 +22995,7 @@ }, { "kind": "ui-localized-call", - "line": 309, + "line": 310, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Off", "surface": "apple", @@ -22995,7 +23003,7 @@ }, { "kind": "ui-localized-call", - "line": 310, + "line": 311, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Setup", "surface": "apple", @@ -23003,7 +23011,7 @@ }, { "kind": "ui-localized-call", - "line": 311, + "line": 312, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Denied", "surface": "apple", @@ -23011,7 +23019,7 @@ }, { "kind": "ui-localized-call", - "line": 312, + "line": 313, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Not Enabled", "surface": "apple", @@ -23019,7 +23027,7 @@ }, { "kind": "ui-localized-call", - "line": 313, + "line": 314, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Unknown", "surface": "apple", @@ -23027,7 +23035,7 @@ }, { "kind": "ui-localized-call", - "line": 320, + "line": 321, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Checking iOS notification permission.", "surface": "apple", @@ -23035,7 +23043,7 @@ }, { "kind": "ui-localized-call", - "line": 323, + "line": 324, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "OpenClaw can show approval prompts and event alerts when the app is not active.", "surface": "apple", @@ -23043,7 +23051,7 @@ }, { "kind": "ui-localized-call", - "line": 325, + "line": 326, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "OpenClaw notifications are off.", "surface": "apple", @@ -23051,7 +23059,7 @@ }, { "kind": "ui-localized-call", - "line": 328, + "line": 329, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Finish notification setup to receive alerts when the app is not active.", "surface": "apple", @@ -23059,7 +23067,7 @@ }, { "kind": "ui-localized-call", - "line": 330, + "line": 331, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Notifications have been denied. Enable them in iOS Settings.", "surface": "apple", @@ -23067,7 +23075,7 @@ }, { "kind": "ui-localized-call", - "line": 333, + "line": 334, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Enable notifications to receive approval prompts and event alerts outside the app.", "surface": "apple", @@ -23075,7 +23083,7 @@ }, { "kind": "ui-localized-call", - "line": 335, + "line": 336, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "OpenClaw cannot determine the current notification permission state.", "surface": "apple", @@ -23083,7 +23091,7 @@ }, { "kind": "ui-call", - "line": 458, + "line": 459, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Connected", "surface": "apple", @@ -23091,7 +23099,7 @@ }, { "kind": "ui-named-argument", - "line": 460, + "line": 461, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Gateway online", "surface": "apple", @@ -23099,7 +23107,7 @@ }, { "kind": "ui-named-argument", - "line": 461, + "line": 462, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Connected to openclaw-gateway.tailnet.ts.net.", "surface": "apple", @@ -23107,7 +23115,7 @@ }, { "kind": "ui-call", - "line": 471, + "line": 472, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Loading", "surface": "apple", @@ -23115,7 +23123,7 @@ }, { "kind": "ui-named-argument", - "line": 473, + "line": 474, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Checking gateway", "surface": "apple", @@ -23123,7 +23131,7 @@ }, { "kind": "ui-named-argument", - "line": 474, + "line": 475, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Refreshing connection, discovery, and device trust state.", "surface": "apple", @@ -23131,7 +23139,7 @@ }, { "kind": "ui-call", - "line": 480, + "line": 481, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Empty", "surface": "apple", @@ -23139,7 +23147,7 @@ }, { "kind": "ui-named-argument", - "line": 482, + "line": 483, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "No gateway configured", "surface": "apple", @@ -23147,7 +23155,7 @@ }, { "kind": "ui-named-argument", - "line": 483, + "line": 484, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Scan a setup QR code, paste a setup code, or choose a discovered gateway.", "surface": "apple", @@ -23155,7 +23163,7 @@ }, { "kind": "ui-call", - "line": 489, + "line": 490, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Error", "surface": "apple", @@ -23163,7 +23171,7 @@ }, { "kind": "ui-named-argument", - "line": 491, + "line": 492, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Tailscale warning", "surface": "apple", @@ -23171,7 +23179,7 @@ }, { "kind": "ui-named-argument", - "line": 492, + "line": 493, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Tailscale is off on this device. Turn it on, then try again.", "surface": "apple", @@ -23179,7 +23187,7 @@ }, { "kind": "ui-call", - "line": 541, + "line": 542, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Address", "surface": "apple", @@ -23187,7 +23195,7 @@ }, { "kind": "ui-call", - "line": 543, + "line": 544, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Server", "surface": "apple", @@ -23195,7 +23203,7 @@ }, { "kind": "ui-call", - "line": 545, + "line": 546, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Discovered", "surface": "apple", @@ -23203,7 +23211,7 @@ }, { "kind": "ui-call", - "line": 547, + "line": 548, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Default Agent", "surface": "apple", @@ -23211,7 +23219,7 @@ }, { "kind": "ui-call", - "line": 569, + "line": 570, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Reconnect", "surface": "apple", @@ -23219,7 +23227,7 @@ }, { "kind": "ui-call", - "line": 570, + "line": 571, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Diagnose", "surface": "apple", @@ -23227,7 +23235,7 @@ }, { "kind": "ui-call", - "line": 579, + "line": 580, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Scan QR", "surface": "apple", @@ -23235,7 +23243,7 @@ }, { "kind": "ui-call", - "line": 580, + "line": 581, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Connect", "surface": "apple", @@ -23243,7 +23251,7 @@ }, { "kind": "ui-call", - "line": 582, + "line": 583, "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", "source": "Discovered gateways and manual setup live here when the gateway has not connected yet.", "surface": "apple", @@ -23793,6 +23801,190 @@ "surface": "apple", "id": "native.apple.7a5a1b9342f01954" }, + { + "kind": "conditional-branch", + "line": 159, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "I’ll keep this conversation separate from ordinary agent chat.", + "surface": "apple", + "id": "native.apple.0706fbaf1d690c8c" + }, + { + "kind": "conditional-branch", + "line": 160, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "I can check Gateway status, repair configuration, change models, or connect channels.", + "surface": "apple", + "id": "native.apple.8904786f15a5e8c6" + }, + { + "kind": "ui-localized-call", + "line": 397, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "", + "surface": "apple", + "id": "native.apple.3c75d45619e216db" + }, + { + "kind": "ui-localized-call", + "line": 456, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "The Gateway connection changed. Restart OpenClaw to reconnect.", + "surface": "apple", + "id": "native.apple.8aee90eb1e1061b9" + }, + { + "kind": "ui-modifier", + "line": 500, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "OpenClaw", + "surface": "apple", + "id": "native.apple.47bc765cd8464c1e" + }, + { + "kind": "ui-call", + "line": 561, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "OpenClaw is working…", + "surface": "apple", + "id": "native.apple.a791a671fb60b5f7" + }, + { + "kind": "ui-localized-call", + "line": 619, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Gateway Required", + "surface": "apple", + "id": "native.apple.bd0e286399723c42" + }, + { + "kind": "ui-localized-call", + "line": 620, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Full Access Required", + "surface": "apple", + "id": "native.apple.7d707323fc24e456" + }, + { + "kind": "ui-localized-call", + "line": 621, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Checking Gateway", + "surface": "apple", + "id": "native.apple.d37a3b497fc2b746" + }, + { + "kind": "ui-localized-call", + "line": 622, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Gateway Update Required", + "surface": "apple", + "id": "native.apple.55474e9051b8aabc" + }, + { + "kind": "ui-localized-call", + "line": 630, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Connect this iPhone to a Gateway before opening the OpenClaw settings assistant.", + "surface": "apple", + "id": "native.apple.441f1770a487a055" + }, + { + "kind": "ui-localized-call", + "line": 632, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Reconnect with operator.admin access to review and change Gateway settings.", + "surface": "apple", + "id": "native.apple.8e0a20f39742ff98" + }, + { + "kind": "ui-localized-call", + "line": 634, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Checking whether this Gateway supports the OpenClaw settings assistant.", + "surface": "apple", + "id": "native.apple.9115f5d67e36fd08" + }, + { + "kind": "ui-localized-call", + "line": 636, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Update this Gateway to use the OpenClaw settings assistant.", + "surface": "apple", + "id": "native.apple.3a7955c72e7ab728" + }, + { + "kind": "ui-call", + "line": 654, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Restart", + "surface": "apple", + "id": "native.apple.fd24248c00007ba3" + }, + { + "kind": "ui-call", + "line": 665, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "OpenClaw is ready to continue in your ordinary chat.", + "surface": "apple", + "id": "native.apple.7ec6b261897ee62c" + }, + { + "kind": "ui-call", + "line": 672, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Open Chat", + "surface": "apple", + "id": "native.apple.ad820dd76c74e3f8" + }, + { + "kind": "ui-modifier", + "line": 695, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Enter secret", + "surface": "apple", + "id": "native.apple.1cfa800d32483cfd" + }, + { + "kind": "ui-call", + "line": 697, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Enter secret…", + "surface": "apple", + "id": "native.apple.7efa99fdc895022e" + }, + { + "kind": "ui-call", + "line": 706, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Reply to OpenClaw…", + "surface": "apple", + "id": "native.apple.e40e721e131e606e" + }, + { + "kind": "ui-modifier", + "line": 728, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Send reply", + "surface": "apple", + "id": "native.apple.5b0adbb8a28011ab" + }, + { + "kind": "ui-call", + "line": 861, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Skip for now", + "surface": "apple", + "id": "native.apple.16be123afdcc66c1" + }, + { + "kind": "ui-call", + "line": 897, + "path": "apps/ios/Sources/Design/SettingsSystemAgentChat.swift", + "source": "Recommended", + "surface": "apple", + "id": "native.apple.441673bf8bef63c3" + }, { "kind": "ui-named-argument", "line": 17, @@ -26283,7 +26475,7 @@ }, { "kind": "ui-named-argument", - "line": 255, + "line": 276, "path": "apps/ios/Sources/RootTabs.swift", "source": "Overview", "surface": "apple", @@ -26291,7 +26483,7 @@ }, { "kind": "ui-named-argument", - "line": 283, + "line": 304, "path": "apps/ios/Sources/RootTabs.swift", "source": "Agents", "surface": "apple", @@ -26299,7 +26491,7 @@ }, { "kind": "ui-named-argument", - "line": 290, + "line": 311, "path": "apps/ios/Sources/RootTabs.swift", "source": "Instances", "surface": "apple", @@ -26307,7 +26499,7 @@ }, { "kind": "ui-named-argument", - "line": 301, + "line": 322, "path": "apps/ios/Sources/RootTabs.swift", "source": "Files", "surface": "apple", @@ -26315,7 +26507,7 @@ }, { "kind": "ui-named-argument", - "line": 308, + "line": 329, "path": "apps/ios/Sources/RootTabs.swift", "source": "Dreaming", "surface": "apple", @@ -26323,7 +26515,7 @@ }, { "kind": "ui-named-argument", - "line": 315, + "line": 336, "path": "apps/ios/Sources/RootTabs.swift", "source": "Usage", "surface": "apple", @@ -26331,7 +26523,7 @@ }, { "kind": "ui-named-argument", - "line": 322, + "line": 343, "path": "apps/ios/Sources/RootTabs.swift", "source": "Automations", "surface": "apple", @@ -26339,7 +26531,7 @@ }, { "kind": "ui-localized-call", - "line": 412, + "line": 433, "path": "apps/ios/Sources/RootTabs.swift", "source": "Hide Sidebar", "surface": "apple", @@ -26347,7 +26539,7 @@ }, { "kind": "ui-localized-call", - "line": 418, + "line": 439, "path": "apps/ios/Sources/RootTabs.swift", "source": "Show Sidebar", "surface": "apple", @@ -26355,7 +26547,7 @@ }, { "kind": "ui-modifier", - "line": 545, + "line": 566, "path": "apps/ios/Sources/RootTabs.swift", "source": "Close canvas", "surface": "apple", @@ -26363,7 +26555,7 @@ }, { "kind": "conditional-branch", - "line": 808, + "line": 829, "path": "apps/ios/Sources/RootTabs.swift", "source": "Gateway needs attention", "surface": "apple", @@ -26371,7 +26563,7 @@ }, { "kind": "conditional-branch", - "line": 808, + "line": 829, "path": "apps/ios/Sources/RootTabs.swift", "source": "OpenClaw iOS", "surface": "apple", @@ -26379,7 +26571,7 @@ }, { "kind": "conditional-branch", - "line": 844, + "line": 865, "path": "apps/ios/Sources/RootTabs.swift", "source": "Available", "surface": "apple", @@ -26387,7 +26579,7 @@ }, { "kind": "conditional-branch", - "line": 844, + "line": 865, "path": "apps/ios/Sources/RootTabs.swift", "source": "Gateway default", "surface": "apple", @@ -26395,7 +26587,7 @@ }, { "kind": "conditional-branch", - "line": 844, + "line": 865, "path": "apps/ios/Sources/RootTabs.swift", "source": "Routed on this phone", "surface": "apple", diff --git a/apps/ios/Sources/Chat/AppleReviewDemoChatTransport.swift b/apps/ios/Sources/Chat/AppleReviewDemoChatTransport.swift index 83334fd61da7..61ceb1011e0c 100644 --- a/apps/ios/Sources/Chat/AppleReviewDemoChatTransport.swift +++ b/apps/ios/Sources/Chat/AppleReviewDemoChatTransport.swift @@ -20,7 +20,7 @@ enum AppleReviewDemoMode { enum ScreenshotFixtureMode { static let gatewayName = "OpenClaw Gateway" - static let gatewayAddress = "Mac Studio on local network" + static let gatewayAddress = "Gateway on local network" static let gatewayID = "screenshot-fixture-gateway" static var agents: [AgentSummary] { diff --git a/apps/ios/Sources/Design/SettingsProTab.swift b/apps/ios/Sources/Design/SettingsProTab.swift index 85b52acdb212..14542fc81845 100644 --- a/apps/ios/Sources/Design/SettingsProTab.swift +++ b/apps/ios/Sources/Design/SettingsProTab.swift @@ -85,6 +85,7 @@ struct SettingsProTab: View { @State var diagnosticsLastRunText = "Not run" @State var diagnosticsIssueCount: Int? @State var showTalkIssueDetails = false + @State var systemAgentChatStore = IOSSystemAgentChatStore() @State private var navigationPath: [SettingsRoute] = [] let initialRoute: SettingsRoute? let directRoute: SettingsRoute? diff --git a/apps/ios/Sources/Design/SettingsProTabActions.swift b/apps/ios/Sources/Design/SettingsProTabActions.swift index 6f461c3cb406..0a3abea0affa 100644 --- a/apps/ios/Sources/Design/SettingsProTabActions.swift +++ b/apps/ios/Sources/Design/SettingsProTabActions.swift @@ -933,6 +933,7 @@ extension SettingsProTab { func title(for route: SettingsRoute) -> String { switch route { case .gateway: String(localized: "Gateway") + case .systemAgent: String(localized: "OpenClaw") case .appleWatch: String(localized: "Apple Watch") case .approvals: String(localized: "Approvals") case .permissions: String(localized: "Permissions") diff --git a/apps/ios/Sources/Design/SettingsProTabSections.swift b/apps/ios/Sources/Design/SettingsProTabSections.swift index 239c5a193cc0..7231baa0c67d 100644 --- a/apps/ios/Sources/Design/SettingsProTabSections.swift +++ b/apps/ios/Sources/Design/SettingsProTabSections.swift @@ -158,6 +158,12 @@ extension SettingsProTab { @ViewBuilder var settingsListSection: some View { Section { + self.settingsListRow( + icon: "sparkles.square.filled.on.square", + iconColor: OpenClawBrand.accent, + title: "OpenClaw", + route: .systemAgent) + .accessibilityIdentifier("settings-system-agent-row") self.settingsListRow( icon: "checkmark.shield.fill", iconColor: self.pendingApproval == nil ? .green : .orange, @@ -245,6 +251,8 @@ extension SettingsProTab { @ViewBuilder func destination(for route: SettingsRoute) -> some View { switch route { + case .systemAgent: + SettingsSystemAgentChatScreen(model: self.systemAgentChatStore.model(for: self.appModel)) case .channels: SettingsChannelsDestination() .navigationTitle(title(for: route)) @@ -258,6 +266,8 @@ extension SettingsProTab { switch route { case .gateway: self.gatewayDestination + case .systemAgent: + EmptyView() case .appleWatch: self.appleWatchDestination case .approvals: diff --git a/apps/ios/Sources/Design/SettingsProTabSupport.swift b/apps/ios/Sources/Design/SettingsProTabSupport.swift index bdb0b4e0fcbb..0ff23457a6fc 100644 --- a/apps/ios/Sources/Design/SettingsProTabSupport.swift +++ b/apps/ios/Sources/Design/SettingsProTabSupport.swift @@ -6,6 +6,7 @@ import UserNotifications enum SettingsRoute: Hashable { case gateway + case systemAgent case appleWatch case approvals case permissions diff --git a/apps/ios/Sources/Design/SettingsSystemAgentChat.swift b/apps/ios/Sources/Design/SettingsSystemAgentChat.swift new file mode 100644 index 000000000000..aaa8101ac457 --- /dev/null +++ b/apps/ios/Sources/Design/SettingsSystemAgentChat.swift @@ -0,0 +1,945 @@ +import Foundation +import Observation +import OpenClawKit +import SwiftUI + +struct IOSSystemAgentChatRouteLease: Sendable { + let route: GatewayNodeSessionRoute? + let request: @Sendable (_ method: String, _ params: [String: AnyCodable], _ timeoutMs: Double) async throws -> Data + let isCurrent: @Sendable () async -> Bool + + static func live(session: GatewayNodeSession, gatewayID: String?) async -> Self? { + guard let route = await session.currentRoute(ifGatewayID: gatewayID) else { return nil } + return Self( + route: route, + request: { method, params, timeoutMs in + try await session.request( + method: method, + params: params, + timeoutMs: timeoutMs, + ifCurrentRoute: route, + distinguishPreDispatchRouteChange: true) + }, + isCurrent: { + await session.currentRoute(ifGatewayID: gatewayID) == route + }) + } +} + +@MainActor +@Observable +final class IOSSystemAgentChatModel { + enum AccessState: Equatable { + case disconnected + case missingAdminScope + case checkingSystemAgentMethod + case missingSystemAgentMethod + case ready + } + + struct Message: Identifiable, Equatable { + enum Role: Equatable { + case assistant + case user + } + + let id: UUID + let role: Role + let text: String + let question: SystemAgentChatQuestion? + + init( + id: UUID = UUID(), + role: Role, + text: String, + question: SystemAgentChatQuestion? = nil) + { + self.id = id + self.role = role + self.text = text + self.question = question + } + } + + struct Handoff: Equatable { + let agentID: String? + } + + typealias CaptureRoute = @Sendable (_ gatewayID: String?) async -> IOSSystemAgentChatRouteLease? + + private struct ChatResult: Decodable { + let reply: String + let action: String + let sensitive: Bool? + let agentId: String? + let question: AnyCodable? + } + + private(set) var messages: [Message] = [] + private(set) var isSending = false + private(set) var errorMessage: String? + private(set) var expectsSensitiveReply = false + private(set) var dismissedQuestionMessageIDs: Set = [] + private(set) var retiredQuestionMessageIDs: Set = [] + private(set) var accessState: AccessState + private(set) var pendingHandoff: Handoff? + private(set) var sessionID: String + var input = "" + var onOpenAgent: ((String?) -> Void)? + + private let sessionPrefix: String + private let captureRoute: CaptureRoute + private var routeLease: IOSSystemAgentChatRouteLease? + private var routeIdentity: String? + private var started = false + private var requestGeneration: UInt64? = 0 + private var requestTask: Task? + private var systemAgentMethodSupport: (gatewayID: String?, route: GatewayNodeSessionRoute, value: Bool)? + + init( + accessState: AccessState, + routeIdentity: String?, + sessionPrefix: String = "ios-settings-openclaw", + captureRoute: @escaping CaptureRoute) + { + self.accessState = accessState + self.routeIdentity = routeIdentity + self.sessionPrefix = sessionPrefix + self.sessionID = "\(sessionPrefix)-\(UUID().uuidString)" + self.captureRoute = captureRoute + } + + convenience init(appModel: NodeAppModel) { + let session = appModel.operatorSession + let captureRoute: CaptureRoute = if appModel.isScreenshotFixtureModeEnabled { + { _ in + IOSSystemAgentChatRouteLease( + route: nil, + request: { _, params, _ in try Self.screenshotFixtureReply(params: params) }, + isCurrent: { true }) + } + } else { + { gatewayID in + await IOSSystemAgentChatRouteLease.live(session: session, gatewayID: gatewayID) + } + } + self.init( + accessState: Self.accessState( + connected: appModel.isOperatorGatewayConnected, + hasAdminScope: appModel.hasOperatorAdminScope, + supportsSystemAgent: appModel.isScreenshotFixtureModeEnabled ? true : nil), + routeIdentity: appModel.connectedGatewayID, + captureRoute: captureRoute) + self.onOpenAgent = { [weak appModel] agentID in + guard let appModel else { return } + let trimmedAgentID = agentID?.trimmingCharacters(in: .whitespacesAndNewlines) + if let trimmedAgentID, !trimmedAgentID.isEmpty { + appModel.setSelectedAgentId(trimmedAgentID) + } + appModel.openChat(sessionKey: nil) + } + } + + static func accessState( + connected: Bool, + hasAdminScope: Bool, + supportsSystemAgent: Bool?) -> AccessState + { + guard connected else { return .disconnected } + guard hasAdminScope else { return .missingAdminScope } + guard let supportsSystemAgent else { return .checkingSystemAgentMethod } + return supportsSystemAgent ? .ready : .missingSystemAgentMethod + } + + private nonisolated static func screenshotFixtureReply(params: [String: AnyCodable]) throws -> Data { + let hasMessage = params["message"]?.value is String + var result: [String: Any] = [ + "sessionId": "ios-screenshot-openclaw", + "reply": hasMessage + ? "I’ll keep this conversation separate from ordinary agent chat." + : "I can check Gateway status, repair configuration, change models, or connect channels.", + "action": "none", + ] + if !hasMessage { + result["question"] = [ + "id": "help", + "header": "OpenClaw", + "question": "What should we look at first?", + "options": [ + [ + "label": "Check status", + "description": "Review the Gateway and active services.", + "recommended": true, + "reply": "Check Gateway status", + ], + [ + "label": "Review setup", + "description": "Inspect models, channels, and configuration.", + "reply": "Review setup", + ], + ], + ] + } + return try JSONSerialization.data(withJSONObject: result) + } + + func updateAccess( + connected: Bool, + hasAdminScope: Bool, + supportsSystemAgent: Bool? = true, + routeIdentity: String?) + { + let nextAccess = Self.accessState( + connected: connected, + hasAdminScope: hasAdminScope, + supportsSystemAgent: supportsSystemAgent) + let routeChanged = self.routeIdentity != routeIdentity + self.routeIdentity = routeIdentity + self.accessState = nextAccess + + if routeChanged { + self.invalidateCurrentRequest() + self.clearSystemAgentMethodSupport() + self.rotateConversation() + return + } + if nextAccess == .checkingSystemAgentMethod { + self.invalidateCurrentRequest() + self.routeLease = nil + self.input = "" + return + } + guard nextAccess != .ready else { return } + guard self.started || self.isSending || !self.messages.isEmpty else { return } + self.invalidateCurrentRequest() + self.routeLease = nil + self.input = "" + self.expectsSensitiveReply = false + self.pendingHandoff = nil + self.errorMessage = String(localized: "The Gateway connection changed. Restart OpenClaw to reconnect.") + } + + @discardableResult + func matchesGatewayIdentity(_ gatewayID: String?) -> Bool { + self.routeIdentity == gatewayID + } + + func cachedSystemAgentMethodSupport( + gatewayID: String?, + route: GatewayNodeSessionRoute) -> Bool? + { + guard let systemAgentMethodSupport, + systemAgentMethodSupport.gatewayID == gatewayID, + systemAgentMethodSupport.route == route + else { return nil } + return systemAgentMethodSupport.value + } + + func cacheSystemAgentMethodSupport( + gatewayID: String?, + route: GatewayNodeSessionRoute, + value: Bool) + { + self.systemAgentMethodSupport = (gatewayID, route, value) + } + + func clearSystemAgentMethodSupport() { + self.systemAgentMethodSupport = nil + } + + private func rotateConversation() { + self.started = false + self.routeLease = nil + self.sessionID = "\(self.sessionPrefix)-\(UUID().uuidString)" + self.messages.removeAll() + self.dismissedQuestionMessageIDs.removeAll() + self.retiredQuestionMessageIDs.removeAll() + self.input = "" + self.errorMessage = nil + self.expectsSensitiveReply = false + self.pendingHandoff = nil + } + + func startIfNeeded() -> Task? { + guard self.accessState == .ready, + !self.started, + self.errorMessage == nil, + let generation = self.requestGeneration + else { return nil } + self.started = true + let task = Task { [weak self] in + guard let self else { return } + await self.requestReply(message: nil, generation: generation) + } + self.requestTask = task + return task + } + + @discardableResult + func send() -> Task? { + let trimmed = self.input.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + return self.send(message: self.expectsSensitiveReply ? self.input : trimmed) + } + + @discardableResult + func answerQuestion(messageID: UUID, optionLabel: String) -> Task? { + guard let message = self.messages.first(where: { $0.id == messageID }), + let question = message.question, + let option = question.options.first(where: { $0.label == optionLabel }), + self.canAnswerQuestion(message) + else { return nil } + return self.send(message: option.reply ?? option.label, displayText: option.label) + } + + @discardableResult + func skipQuestion(messageID: UUID) -> Task? { + guard let message = self.messages.first(where: { $0.id == messageID }), + self.canAnswerQuestion(message), + let task = self.send( + message: "Skip for now", + displayText: String(localized: "Skip for now")) + else { return nil } + self.dismissedQuestionMessageIDs.insert(messageID) + return task + } + + func isQuestionVisible(_ message: Message) -> Bool { + message.question != nil && !self.dismissedQuestionMessageIDs.contains(message.id) + } + + func canAnswerQuestion(_ message: Message) -> Bool { + self.accessState == .ready && + self.isQuestionVisible(message) && + !self.retiredQuestionMessageIDs.contains(message.id) && + !self.isSending && + self.errorMessage == nil + } + + func openAgent() { + guard let handoff = self.pendingHandoff else { return } + self.pendingHandoff = nil + self.onOpenAgent?(handoff.agentID) + } + + @discardableResult + func restartAfterError() -> Task? { + guard self.accessState == .ready, + let previousGeneration = self.requestGeneration + else { return nil } + let generation = previousGeneration &+ 1 + self.requestGeneration = generation + self.requestTask?.cancel() + self.routeLease = nil + self.sessionID = "\(self.sessionPrefix)-\(UUID().uuidString)" + self.started = true + self.messages.removeAll() + self.dismissedQuestionMessageIDs.removeAll() + self.retiredQuestionMessageIDs.removeAll() + self.input = "" + self.errorMessage = nil + self.expectsSensitiveReply = false + self.pendingHandoff = nil + let task = Task { [weak self] in + guard let self else { return } + await self.requestReply(message: nil, generation: generation) + } + self.requestTask = task + return task + } + + /// A secret-bearing draft must not survive while another surface is active. + /// In-flight work stays bound to its captured route and can finish in the retained model. + func clearInputForBackground() { + self.input = "" + } + + private func invalidateCurrentRequest() { + guard let generation = self.requestGeneration else { return } + self.requestGeneration = generation &+ 1 + self.requestTask?.cancel() + self.requestTask = nil + self.isSending = false + } + + private func isCurrentRequest(_ generation: UInt64) -> Bool { + self.requestGeneration == generation && !Task.isCancelled + } + + private func sessionRoute(for generation: UInt64) async throws -> IOSSystemAgentChatRouteLease { + if let routeLease = self.routeLease { + return routeLease + } + guard let routeLease = await self.captureRoute(self.routeIdentity) else { + guard self.isCurrentRequest(generation) else { throw CancellationError() } + throw NSError( + domain: "Gateway", + code: 0, + userInfo: [NSLocalizedDescriptionKey: "Gateway is not connected"]) + } + guard self.isCurrentRequest(generation) else { throw CancellationError() } + self.routeLease = routeLease + return routeLease + } + + private func send(message: String, displayText: String? = nil) -> Task? { + guard self.accessState == .ready, + let generation = self.requestGeneration, + !message.isEmpty, + !self.isSending, + self.errorMessage == nil, + self.pendingHandoff == nil + else { return nil } + self.retireQuestions() + self.input = "" + self.messages.append(Message( + role: .user, + text: displayText ?? (self.expectsSensitiveReply ? String(localized: "") : message))) + let task = Task { [weak self] in + guard let self else { return } + await self.requestReply(message: message, generation: generation) + } + self.requestTask = task + return task + } + + private func retireQuestions() { + for message in self.messages where message.question != nil { + self.retiredQuestionMessageIDs.insert(message.id) + } + } + + private func requestReply(message: String?, generation: UInt64) async { + guard self.accessState == .ready, self.isCurrentRequest(generation) else { return } + self.isSending = true + self.errorMessage = nil + defer { + if self.requestGeneration == generation { + self.isSending = false + } + } + + do { + var params: [String: AnyCodable] = [ + "sessionId": AnyCodable(self.sessionID), + ] + if let message { + params["message"] = AnyCodable(message) + } + let routeLease = try await self.sessionRoute(for: generation) + guard self.isCurrentRequest(generation) else { return } + let data = try await routeLease.request("openclaw.chat", params, 190_000) + guard self.isCurrentRequest(generation) else { return } + guard await routeLease.isCurrent() else { throw CancellationError() } + let result = try JSONDecoder().decode(ChatResult.self, from: data) + guard self.isCurrentRequest(generation) else { return } + self.expectsSensitiveReply = result.sensitive == true + self.messages.append(Message( + role: .assistant, + text: result.reply, + question: SystemAgentChatQuestion.parse(result.question?.dictionaryValue))) + if result.action == "open-agent" { + self.pendingHandoff = Handoff(agentID: result.agentId) + } + } catch { + guard self.requestGeneration == generation else { return } + let routeChangedBeforeDispatch = if let requestError = error as? GatewayNodeSessionRequestError { + switch requestError { + case .routeChangedBeforeDispatch: true + } + } else { + false + } + if error is CancellationError || routeChangedBeforeDispatch { + self.started = false + self.routeLease = nil + self.errorMessage = String(localized: "The Gateway connection changed. Restart OpenClaw to reconnect.") + return + } + self.errorMessage = error.localizedDescription + } + } +} + +@MainActor +@Observable +final class IOSSystemAgentChatStore { + private var model: IOSSystemAgentChatModel? + + func model(for appModel: NodeAppModel) -> IOSSystemAgentChatModel { + if let model { + return model + } + let model = IOSSystemAgentChatModel(appModel: appModel) + self.model = model + return model + } +} + +struct SettingsSystemAgentChatScreen: View { + @Environment(NodeAppModel.self) private var appModel + @Environment(\.scenePhase) private var scenePhase + @State private var model: IOSSystemAgentChatModel + @State private var systemAgentSupportCheckID = UUID() + @State private var systemAgentSupportRetryTask: Task? + @State private var isScreenActive = false + + init(model: IOSSystemAgentChatModel) { + _model = State(initialValue: model) + } + + var body: some View { + VStack(spacing: 0) { + if self.model.accessState == .ready { + self.chatContent + } else { + self.accessGate + } + } + .background(Color(uiColor: .systemGroupedBackground)) + .navigationTitle("OpenClaw") + .navigationBarTitleDisplayMode(.inline) + .task { + self.isScreenActive = true + await self.refreshSystemAgentSupportAndStart() + } + .onChange(of: self.appModel.isOperatorGatewayConnected) { _, _ in + Task { await self.refreshSystemAgentSupportAndStart(forceRefresh: true) } + } + .onChange(of: self.appModel.hasOperatorAdminScope) { _, _ in + Task { await self.refreshSystemAgentSupportAndStart(forceRefresh: true) } + } + .onChange(of: self.appModel.connectedGatewayID) { _, _ in + Task { await self.refreshSystemAgentSupportAndStart(forceRefresh: true) } + } + .onChange(of: self.scenePhase) { _, phase in + guard phase == .active else { + self.cancelSystemAgentSupportRetry() + self.model.clearInputForBackground() + return + } + Task { await self.refreshSystemAgentSupportAndStart() } + } + .onDisappear { + self.isScreenActive = false + self.cancelSystemAgentSupportRetry() + self.model.clearInputForBackground() + } + } + + private var chatContent: some View { + VStack(spacing: 10) { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 12) { + ForEach(self.model.messages) { message in + VStack(alignment: .leading, spacing: 10) { + IOSSystemAgentChatBubble(message: message) + if let question = message.question, + self.model.isQuestionVisible(message) + { + IOSSystemAgentQuestionCard( + question: question, + isEnabled: self.model.canAnswerQuestion(message), + onSelect: { option in + self.model.answerQuestion( + messageID: message.id, + optionLabel: option.label) + }, + onSkip: { + self.model.skipQuestion(messageID: message.id) + }) + } + } + .id(message.id) + } + + if self.model.isSending { + HStack(spacing: 8) { + ProgressView() + .controlSize(.small) + Text("OpenClaw is working…") + .font(OpenClawType.caption) + .foregroundStyle(.secondary) + } + .padding(.horizontal, 4) + } + } + .padding(.horizontal, 14) + .padding(.vertical, 12) + } + .onChange(of: self.model.messages) { _, messages in + guard let last = messages.last else { return } + withAnimation { proxy.scrollTo(last.id, anchor: .bottom) } + } + } + + if let error = self.model.errorMessage { + self.errorRow(error) + } + if self.model.pendingHandoff != nil { + self.handoffRow + } else { + self.composer + } + } + } + + private var accessGate: some View { + VStack(spacing: 14) { + Image(systemName: self.accessGateIcon) + .font(.system(size: 42, weight: .semibold)) + .foregroundStyle(OpenClawBrand.warn) + Text(self.accessGateTitle) + .font(OpenClawType.title3SemiBold) + Text(self.accessGateDetail) + .font(OpenClawType.body) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: 420) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(28) + .accessibilityIdentifier("settings-system-agent-access-gate") + } + + private var accessGateIcon: String { + switch self.model.accessState { + case .disconnected: "wifi.slash" + case .missingAdminScope: "lock.shield" + case .checkingSystemAgentMethod: "arrow.triangle.2.circlepath" + case .missingSystemAgentMethod: "arrow.down.circle" + case .ready: "" + } + } + + private var accessGateTitle: String { + switch self.model.accessState { + case .disconnected: String(localized: "Gateway Required") + case .missingAdminScope: String(localized: "Full Access Required") + case .checkingSystemAgentMethod: String(localized: "Checking Gateway") + case .missingSystemAgentMethod: String(localized: "Gateway Update Required") + case .ready: "" + } + } + + private var accessGateDetail: String { + switch self.model.accessState { + case .disconnected: + String(localized: "Connect this iPhone to a Gateway before opening the OpenClaw settings assistant.") + case .missingAdminScope: + String(localized: "Reconnect with operator.admin access to review and change Gateway settings.") + case .checkingSystemAgentMethod: + String(localized: "Checking whether this Gateway supports the OpenClaw settings assistant.") + case .missingSystemAgentMethod: + String(localized: "Update this Gateway to use the OpenClaw settings assistant.") + case .ready: + "" + } + } + + private func errorRow(_ error: String) -> some View { + HStack(spacing: 10) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundStyle(OpenClawBrand.warn) + Text(error) + .font(OpenClawType.caption) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + Spacer(minLength: 0) + Button { + self.model.restartAfterError() + } label: { + Text("Restart") + .font(OpenClawType.captionSemiBold) + } + .buttonStyle(.bordered) + .controlSize(.small) + } + .padding(.horizontal, 14) + } + + private var handoffRow: some View { + VStack(alignment: .leading, spacing: 8) { + Text("OpenClaw is ready to continue in your ordinary chat.") + .font(OpenClawType.subhead) + .foregroundStyle(.secondary) + Button { + self.model.openAgent() + } label: { + Label { + Text("Open Chat") + .font(OpenClawType.subheadSemiBold) + } icon: { + Image(systemName: "bubble.left.and.bubble.right.fill") + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .accessibilityIdentifier("settings-system-agent-open-chat") + } + .padding(.horizontal, 14) + .padding(.bottom, 12) + } + + private var composer: some View { + HStack(alignment: .bottom, spacing: 10) { + Group { + if self.model.expectsSensitiveReply { + ZStack(alignment: .leading) { + SecureField("", text: self.$model.input) + .font(OpenClawType.body) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + .accessibilityLabel("Enter secret") + if self.model.input.isEmpty { + Text("Enter secret…") + .font(OpenClawType.body) + .foregroundStyle(.tertiary) + .allowsHitTesting(false) + .accessibilityHidden(true) + } + } + } else { + TextField(text: self.$model.input, axis: .vertical) { + Text("Reply to OpenClaw…") + .font(OpenClawType.body) + } + .font(OpenClawType.body) + .lineLimit(1...5) + } + } + .textFieldStyle(.roundedBorder) + .onSubmit { self.model.send() } + .disabled(self.model.errorMessage != nil || self.model.isSending) + + Button { + self.model.send() + } label: { + Image(systemName: "arrow.up.circle.fill") + .font(.system(size: 28, weight: .semibold)) + } + .buttonStyle(.plain) + .disabled( + self.model.isSending || + self.model.errorMessage != nil || + self.model.input.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .accessibilityLabel("Send reply") + } + .padding(.horizontal, 14) + .padding(.bottom, 12) + } + + private func isCurrentSystemAgentSupportCheck(_ checkID: UUID, gatewayID: String?) -> Bool { + self.isScreenActive && + self.scenePhase == .active && + self.systemAgentSupportCheckID == checkID && + self.appModel.connectedGatewayID == gatewayID + } + + private func cancelSystemAgentSupportRetry() { + self.systemAgentSupportRetryTask?.cancel() + self.systemAgentSupportRetryTask = nil + } + + private func enterCheckingSystemAgentSupport(gatewayID: String?) { + self.model.clearSystemAgentMethodSupport() + self.model.updateAccess( + connected: self.appModel.isOperatorGatewayConnected, + hasAdminScope: self.appModel.hasOperatorAdminScope, + supportsSystemAgent: nil, + routeIdentity: gatewayID) + } + + private func retrySystemAgentSupportCheck(_ checkID: UUID, gatewayID: String?) { + self.cancelSystemAgentSupportRetry() + self.systemAgentSupportRetryTask = Task { @MainActor in + try? await Task.sleep(for: .seconds(1)) + guard !Task.isCancelled, + self.isScreenActive, + self.scenePhase == .active, + self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) + else { return } + self.systemAgentSupportRetryTask = nil + await self.refreshSystemAgentSupportAndStart() + } + } + + private func refreshSystemAgentSupportAndStart(forceRefresh: Bool = false) async { + guard self.isScreenActive, self.scenePhase == .active else { return } + let checkID = UUID() + self.systemAgentSupportCheckID = checkID + let gatewayID = self.appModel.connectedGatewayID + let connected = self.appModel.isOperatorGatewayConnected + let hasAdminScope = self.appModel.hasOperatorAdminScope + let isFixture = self.appModel.isScreenshotFixtureModeEnabled + + if forceRefresh || !connected || !hasAdminScope || !self.model.matchesGatewayIdentity(gatewayID) { + self.enterCheckingSystemAgentSupport(gatewayID: gatewayID) + } + + guard connected, hasAdminScope else { return } + if isFixture { + self.model.updateAccess( + connected: connected, + hasAdminScope: hasAdminScope, + supportsSystemAgent: true, + routeIdentity: gatewayID) + self.model.startIfNeeded() + return + } + + guard let route = await self.appModel.operatorSession.currentRoute(ifGatewayID: gatewayID) else { + guard self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) else { return } + self.enterCheckingSystemAgentSupport(gatewayID: gatewayID) + self.retrySystemAgentSupportCheck(checkID, gatewayID: gatewayID) + return + } + guard self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) else { return } + + if !forceRefresh, + let support = self.model.cachedSystemAgentMethodSupport(gatewayID: gatewayID, route: route) + { + self.model.updateAccess( + connected: connected, + hasAdminScope: hasAdminScope, + supportsSystemAgent: support, + routeIdentity: gatewayID) + self.model.startIfNeeded() + return + } + + self.enterCheckingSystemAgentSupport(gatewayID: gatewayID) + let support = await self.appModel.operatorSession.supportsServerMethod( + "openclaw.chat", + ifCurrentRoute: route) + guard self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) else { return } + guard let currentRoute = await self.appModel.operatorSession.currentRoute(ifGatewayID: gatewayID) else { + guard self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) else { return } + self.enterCheckingSystemAgentSupport(gatewayID: gatewayID) + self.retrySystemAgentSupportCheck(checkID, gatewayID: gatewayID) + return + } + guard self.isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID) else { return } + guard currentRoute == route else { + self.enterCheckingSystemAgentSupport(gatewayID: gatewayID) + self.retrySystemAgentSupportCheck(checkID, gatewayID: gatewayID) + return + } + guard let support else { + self.retrySystemAgentSupportCheck(checkID, gatewayID: gatewayID) + return + } + self.model.cacheSystemAgentMethodSupport(gatewayID: gatewayID, route: route, value: support) + self.model.updateAccess( + connected: self.appModel.isOperatorGatewayConnected, + hasAdminScope: self.appModel.hasOperatorAdminScope, + supportsSystemAgent: support, + routeIdentity: gatewayID) + self.model.startIfNeeded() + } +} + +private struct IOSSystemAgentQuestionCard: View { + let question: SystemAgentChatQuestion + let isEnabled: Bool + let onSelect: (SystemAgentChatQuestion.Option) -> Void + let onSkip: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + Text(self.question.header.uppercased()) + .font(OpenClawType.caption2SemiBold) + .foregroundStyle(OpenClawBrand.accent) + Text(self.question.question) + .font(OpenClawType.subheadSemiBold) + ForEach(self.question.options, id: \.label) { option in + self.optionButton(option) + } + Button(action: self.onSkip) { + Text("Skip for now") + .font(OpenClawType.captionSemiBold) + } + .buttonStyle(.plain) + .foregroundStyle(.secondary) + .disabled(!self.isEnabled) + } + .padding(12) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .fill(Color(uiColor: .secondarySystemGroupedBackground))) + .overlay( + RoundedRectangle(cornerRadius: 14, style: .continuous) + .stroke(Color.secondary.opacity(0.16))) + .accessibilityElement(children: .contain) + .accessibilityLabel(self.question.question) + } + + private func optionButton(_ option: SystemAgentChatQuestion.Option) -> some View { + Button { + self.onSelect(option) + } label: { + HStack(alignment: .top, spacing: 10) { + VStack(alignment: .leading, spacing: 3) { + Text(option.label) + .font(OpenClawType.subheadSemiBold) + .foregroundStyle(.primary) + if let description = option.description { + Text(description) + .font(OpenClawType.caption) + .foregroundStyle(.secondary) + } + } + Spacer(minLength: 8) + if option.recommended { + Text("Recommended") + .font(OpenClawType.caption2SemiBold) + .foregroundStyle(OpenClawBrand.accent) + } + } + .padding(.horizontal, 11) + .padding(.vertical, 9) + .frame(maxWidth: .infinity, alignment: .leading) + .background( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .fill(option.recommended + ? OpenClawBrand.accent.opacity(0.12) + : Color(uiColor: .tertiarySystemGroupedBackground))) + .overlay( + RoundedRectangle(cornerRadius: 10, style: .continuous) + .stroke(option.recommended + ? OpenClawBrand.accent.opacity(0.55) + : Color.secondary.opacity(0.12))) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .disabled(!self.isEnabled) + } +} + +private struct IOSSystemAgentChatBubble: View { + let message: IOSSystemAgentChatModel.Message + + var body: some View { + HStack { + if self.message.role == .user { + Spacer(minLength: 44) + } + Text(self.message.text) + .font(OpenClawType.body) + .textSelection(.enabled) + .padding(.horizontal, 12) + .padding(.vertical, 9) + .background( + RoundedRectangle(cornerRadius: 13, style: .continuous) + .fill(self.message.role == .user + ? OpenClawBrand.accent.opacity(0.18) + : Color(uiColor: .secondarySystemGroupedBackground))) + if self.message.role == .assistant { + Spacer(minLength: 44) + } + } + } +} diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 878572e3cae5..f40e4b6ce2bb 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -20,8 +20,10 @@ struct RootTabs: View { @AppStorage("onboarding.quickSetupDismissed") private var quickSetupDismissed: Bool = false @AppStorage("canvas.debugStatusEnabled") private var canvasDebugStatusEnabled: Bool = false @State private var selectedSidebarDestination: SidebarDestination = Self.initialSidebarDestination - @State private var selectedSettingsRoute: SettingsRoute? = Self.initialSidebarDestination.settingsRoute - @State private var activeSettingsRoute: SettingsRoute? = Self.initialSidebarDestination.settingsRoute + @State private var selectedSettingsRoute: SettingsRoute? = + Self.initialSettingsRoute ?? Self.initialSidebarDestination.settingsRoute + @State private var activeSettingsRoute: SettingsRoute? = + Self.initialSettingsRoute ?? Self.initialSidebarDestination.settingsRoute @State private var selectedSettingsRouteRequestID: Int = 0 @State private var sidebarModel = RootSidebarModel() // Embedded Settings rows push onto the sidebar stack; clear it before @@ -57,7 +59,14 @@ struct RootTabs: View { initialDestination(arguments: ProcessInfo.processInfo.arguments) } + private static var initialSettingsRoute: SettingsRoute? { + requestedInitialSettingsRoute(arguments: ProcessInfo.processInfo.arguments) + } + static func initialDestination(arguments: [String]) -> SidebarDestination { + if self.requestedInitialSettingsRoute(arguments: arguments) != nil { + return .settings + } if let requested = self.requestedInitialSidebarDestination(arguments: arguments) { return requested } @@ -73,6 +82,18 @@ struct RootTabs: View { } } + static func requestedInitialSettingsRoute(arguments: [String]) -> SettingsRoute? { + guard let flagIndex = arguments.firstIndex(of: "--openclaw-settings-route") else { + return nil + } + let valueIndex = arguments.index(after: flagIndex) + guard arguments.indices.contains(valueIndex) else { return nil } + return switch arguments[valueIndex].trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { + case "openclaw", "system-agent": .systemAgent + default: nil + } + } + static func requestedInitialSidebarDestination(arguments: [String]) -> SidebarDestination? { guard let flagIndex = arguments.firstIndex(of: "--openclaw-initial-destination") else { return nil diff --git a/apps/ios/Tests/IOSSystemAgentChatTests.swift b/apps/ios/Tests/IOSSystemAgentChatTests.swift new file mode 100644 index 000000000000..1fc7fb83e0e3 --- /dev/null +++ b/apps/ios/Tests/IOSSystemAgentChatTests.swift @@ -0,0 +1,519 @@ +import Foundation +import OpenClawKit +import Testing +@testable import OpenClaw + +@MainActor +struct IOSSystemAgentChatTests { + private struct RecordedRequest: @unchecked Sendable { + let method: String + let params: [String: AnyCodable] + let timeoutMs: Double + } + + private enum HarnessError: Error { + case failed + } + + private actor RequestRecorder { + private var requests: [RecordedRequest] = [] + private var responses: [Result] + + init(responses: [Result]) { + self.responses = responses + } + + func perform( + method: String, + params: [String: AnyCodable], + timeoutMs: Double) throws -> Data + { + self.requests.append(RecordedRequest(method: method, params: params, timeoutMs: timeoutMs)) + guard !self.responses.isEmpty else { throw HarnessError.failed } + return try self.responses.removeFirst().get() + } + + func allRequests() -> [RecordedRequest] { + self.requests + } + } + + private actor RouteState { + private var current = true + + func setCurrent(_ current: Bool) { + self.current = current + } + + func isCurrent() -> Bool { + self.current + } + } + + private actor SuspendedRequest { + private var continuation: CheckedContinuation? + private var request: RecordedRequest? + + func perform(method: String, params: [String: AnyCodable], timeoutMs: Double) async -> Data { + self.request = RecordedRequest(method: method, params: params, timeoutMs: timeoutMs) + return await withCheckedContinuation { continuation in + self.continuation = continuation + } + } + + func hasRequest() -> Bool { + self.request != nil + } + + func resolve(_ data: Data) { + self.continuation?.resume(returning: data) + self.continuation = nil + } + } + + @Test func `RPC is gated by connected operator admin and omits onboarding params`() async throws { + let recorder = RequestRecorder(responses: [.success(Self.reply("Ready"))]) + let model = self.makeModel( + accessState: .disconnected, + recorder: recorder) + + await Self.start(model) + #expect(await recorder.allRequests().isEmpty) + + model.updateAccess(connected: true, hasAdminScope: false, routeIdentity: "gateway-a") + await Self.start(model) + #expect(await recorder.allRequests().isEmpty) + + model.updateAccess(connected: true, hasAdminScope: true, routeIdentity: "gateway-a") + await Self.start(model) + + let request = try #require(await recorder.allRequests().first) + #expect(request.method == "openclaw.chat") + #expect(request.timeoutMs == 190_000) + #expect((request.params["sessionId"]?.value as? String)?.hasPrefix("ios-settings-openclaw-") == true) + #expect(request.params["sessionId"]?.value as? String != "main") + #expect(request.params["message"] == nil) + #expect(request.params["welcomeVariant"] == nil) + #expect(request.params["delegation"] == nil) + } + + @Test func `missing advertised system-agent method blocks the chat`() { + #expect( + IOSSystemAgentChatModel.accessState( + connected: true, + hasAdminScope: true, + supportsSystemAgent: false) == .missingSystemAgentMethod) + } + + @Test func `pending method support check blocks the chat without losing secure state`() async { + let recorder = RequestRecorder(responses: [.success(Self.reply("Enter a secret", sensitive: true))]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + #expect(model.expectsSensitiveReply) + model.updateAccess( + connected: true, + hasAdminScope: true, + supportsSystemAgent: nil, + routeIdentity: "gateway-a") + await Self.start(model) + + #expect(model.accessState == .checkingSystemAgentMethod) + #expect(model.expectsSensitiveReply) + #expect(await recorder.allRequests().count == 1) + } + + @Test func `route change invalidates suspended generation and ignores its reply`() async { + let suspended = SuspendedRequest() + let model = IOSSystemAgentChatModel( + accessState: .ready, + routeIdentity: "gateway-a", + captureRoute: { _ in + IOSSystemAgentChatRouteLease( + route: nil, + request: { method, params, timeoutMs in + await suspended.perform(method: method, params: params, timeoutMs: timeoutMs) + }, + isCurrent: { true }) + }) + + let start = model.startIfNeeded() + await Self.waitUntil { await suspended.hasRequest() } + let originalSessionID = model.sessionID + model.input = "unsent-secret" + model.updateAccess(connected: true, hasAdminScope: true, routeIdentity: "gateway-b") + await suspended.resolve(Self.reply("stale reply")) + await start?.value + + #expect(model.messages.isEmpty) + #expect(model.sessionID != originalSessionID) + #expect(model.input.isEmpty) + #expect(model.errorMessage == nil) + } + + @Test func `gateway identity changes rotate the retained conversation`() async { + let recorder = RequestRecorder(responses: [.success(Self.questionReply())]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + let originalSessionID = model.sessionID + #expect(!model.messages.isEmpty) + + model.updateAccess(connected: true, hasAdminScope: true, routeIdentity: "gateway-b") + + #expect(model.sessionID != originalSessionID) + #expect(model.messages.isEmpty) + #expect(model.dismissedQuestionMessageIDs.isEmpty) + #expect(model.retiredQuestionMessageIDs.isEmpty) + #expect(model.pendingHandoff == nil) + #expect(model.errorMessage == nil) + } + + @Test func `stale route after RPC is rejected`() async { + let recorder = RequestRecorder(responses: [.success(Self.reply("stale reply"))]) + let routeState = RouteState() + await routeState.setCurrent(false) + let model = self.makeModel(recorder: recorder, routeState: routeState) + + await Self.start(model) + + #expect(model.messages.isEmpty) + #expect(model.errorMessage == "The Gateway connection changed. Restart OpenClaw to reconnect.") + } + + @Test func `sensitive answer stays redacted locally and is sent verbatim`() async throws { + let recorder = RequestRecorder(responses: [ + .success(Self.reply("Enter the token", sensitive: true)), + .success(Self.reply("Saved", sensitive: false)), + ]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + #expect(model.expectsSensitiveReply) + model.input = " super-secret-value " + let send = try #require(model.send()) + await send.value + + let requests = await recorder.allRequests() + #expect(requests.count == 2) + #expect(requests[1].params["message"]?.value as? String == " super-secret-value ") + #expect(model.messages.contains { $0.role == .user && $0.text == "" }) + #expect(!model.messages.contains { $0.text.contains("super-secret-value") }) + } + + @Test func `option reply uses canonical value while transcript keeps label`() async throws { + let recorder = RequestRecorder(responses: [ + .success(Self.questionReply()), + .success(Self.reply("Applied")), + ]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + let questionMessage = try #require(model.messages.first) + let answer = try #require(model.answerQuestion(messageID: questionMessage.id, optionLabel: "Use Tailscale")) + await answer.value + + let requests = await recorder.allRequests() + #expect(requests[1].params["message"]?.value as? String == "tailscale") + #expect(model.messages.contains { $0.role == .user && $0.text == "Use Tailscale" }) + #expect(model.retiredQuestionMessageIDs.contains(questionMessage.id)) + } + + @Test func `skip for now sends explicit reply and dismisses card`() async throws { + let recorder = RequestRecorder(responses: [ + .success(Self.questionReply()), + .success(Self.reply("Skipped")), + ]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + let questionMessage = try #require(model.messages.first) + let skip = try #require(model.skipQuestion(messageID: questionMessage.id)) + await skip.value + + let requests = await recorder.allRequests() + #expect(requests[1].params["message"]?.value as? String == "Skip for now") + #expect(model.dismissedQuestionMessageIDs.contains(questionMessage.id)) + #expect(!model.isQuestionVisible(questionMessage)) + } + + @Test func `ordinary gateway errors remain visible instead of becoming route change`() async { + let recorder = RequestRecorder(responses: [.failure(.failed)]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + + #expect(model.errorMessage != nil) + #expect(model.errorMessage != "The Gateway connection changed. Restart OpenClaw to reconnect.") + } + + @Test func `pre-dispatch route change asks for restart`() async { + let model = IOSSystemAgentChatModel( + accessState: .ready, + routeIdentity: "gateway-a", + captureRoute: { _ in + IOSSystemAgentChatRouteLease( + route: nil, + request: { _, _, _ in + throw GatewayNodeSessionRequestError.routeChangedBeforeDispatch + }, + isCurrent: { true }) + }) + + await Self.start(model) + + #expect(model.errorMessage == "The Gateway connection changed. Restart OpenClaw to reconnect.") + } + + @Test func `leaving settings clears input without canceling an in-flight turn`() async { + let suspended = SuspendedRequest() + let model = IOSSystemAgentChatModel( + accessState: .ready, + routeIdentity: "gateway-a", + captureRoute: { _ in + IOSSystemAgentChatRouteLease( + route: nil, + request: { method, params, timeoutMs in + await suspended.perform(method: method, params: params, timeoutMs: timeoutMs) + }, + isCurrent: { true }) + }) + + let start = model.startIfNeeded() + await Self.waitUntil { await suspended.hasRequest() } + model.input = "discard-me" + model.clearInputForBackground() + await suspended.resolve(Self.reply("Welcome")) + await start?.value + + #expect(model.input.isEmpty) + #expect(model.messages.map(\.text) == ["Welcome"]) + #expect(model.errorMessage == nil) + } + + @Test func `returning to settings continues the existing conversation`() async throws { + let recorder = RequestRecorder(responses: [ + .success(Self.reply("Welcome")), + .success(Self.reply("Still here")), + ]) + let model = self.makeModel(recorder: recorder) + + await Self.start(model) + let sessionID = model.sessionID + model.clearInputForBackground() + model.input = "Continue" + let send = try #require(model.send()) + await send.value + + let requests = await recorder.allRequests() + #expect(requests.count == 2) + #expect(requests[1].params["sessionId"]?.value as? String == sessionID) + #expect(requests[1].params["message"]?.value as? String == "Continue") + #expect(model.messages.map(\.text) == ["Welcome", "Continue", "Still here"]) + } + + @Test func `restart creates a fresh system session`() async throws { + let recorder = RequestRecorder(responses: [ + .failure(.failed), + .success(Self.reply("Recovered")), + ]) + let model = self.makeModel(recorder: recorder) + let originalSessionID = model.sessionID + + await Self.start(model) + #expect(model.errorMessage != nil) + let restart = try #require(model.restartAfterError()) + await restart.value + + #expect(model.sessionID != originalSessionID) + #expect(model.messages.map(\.text) == ["Recovered"]) + let requests = await recorder.allRequests() + #expect(requests.count == 2) + #expect(requests[0].params["sessionId"]?.value as? String == originalSessionID) + #expect(requests[1].params["sessionId"]?.value as? String == model.sessionID) + } + + @Test func `open agent handoff waits for explicit action and carries agent`() async { + let recorder = RequestRecorder(responses: [ + .success(Self.reply("Continue in chat", action: "open-agent", agentID: " reviewer ")), + ]) + let model = self.makeModel(recorder: recorder) + var openedAgentID: String? + var openCount = 0 + model.onOpenAgent = { agentID in + openCount += 1 + openedAgentID = agentID + } + + await Self.start(model) + #expect(model.pendingHandoff?.agentID == " reviewer ") + #expect(openCount == 0) + + model.openAgent() + #expect(openCount == 1) + #expect(openedAgentID == " reviewer ") + #expect(model.pendingHandoff == nil) + } + + @Test func `live handoff selects returned agent and opens ordinary chat`() { + let appModel = NodeAppModel() + let model = IOSSystemAgentChatModel(appModel: appModel) + let previousOpenRequest = appModel.openChatRequestID + + model.onOpenAgent?(" reviewer ") + + #expect(appModel.selectedAgentId == "reviewer") + #expect(appModel.openChatRequestID == previousOpenRequest + 1) + } + + @Test func `settings chat model is retained by the settings store`() { + let appModel = NodeAppModel() + let store = IOSSystemAgentChatStore() + + #expect(store.model(for: appModel) === store.model(for: appModel)) + } + + @Test func `settings chat uses branded and accessible secure input typography`() throws { + let iosRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + let source = try String( + contentsOf: iosRoot.appendingPathComponent("Sources/Design/SettingsSystemAgentChat.swift"), + encoding: .utf8) + + #expect(source.contains(".font(OpenClawType.title3SemiBold)")) + #expect(source.contains(".font(OpenClawType.body)")) + #expect(source.contains(".font(OpenClawType.subheadSemiBold)")) + #expect(source.contains(".font(OpenClawType.caption)")) + #expect(source.contains("SecureField(\"\", text: self.$model.input)")) + #expect(source.contains(".onChange(of: self.scenePhase)")) + #expect(source.contains("guard phase == .active else")) + #expect(source.contains("self.cancelSystemAgentSupportRetry()")) + #expect(source.contains(".accessibilityLabel(\"Enter secret\")")) + #expect(source.contains("currentRoute(ifGatewayID: gatewayID)")) + #expect(source.contains("supportsServerMethod(\n \"openclaw.chat\"")) + #expect(source.contains("matchesGatewayIdentity(gatewayID)")) + #expect(source.contains("cachedSystemAgentMethodSupport(gatewayID: gatewayID, route: route)")) + #expect(source.contains("isCurrentSystemAgentSupportCheck(checkID, gatewayID: gatewayID)")) + #expect(source.contains("currentRoute == route")) + #expect(source.contains("retrySystemAgentSupportCheck(checkID, gatewayID: gatewayID)")) + #expect(source.contains("cancelSystemAgentSupportRetry()")) + #expect(source.contains("enterCheckingSystemAgentSupport(gatewayID: gatewayID)")) + #expect(source.contains("self.isScreenActive")) + #expect(source.contains("guard let support else")) + #expect(source.contains("String(localized: \"Gateway Update Required\")")) + #expect(source.contains("String(localized: \"Skip for now\")")) + #expect(source.contains("String(localized: \"\")")) + #expect(!source.contains("SecureField(\"Enter secret")) + } + + @Test func `settings route launch argument opens OpenClaw directly`() { + let arguments = ["OpenClaw", "--openclaw-settings-route", "openclaw"] + + #expect(RootTabs.requestedInitialSettingsRoute(arguments: arguments) == .systemAgent) + #expect(RootTabs.initialDestination(arguments: arguments) == .settings) + } + + @Test func `settings route is visible and handoff uses root chat navigation`() throws { + #expect(SettingsProTab().title(for: .systemAgent) == "OpenClaw") + + let iosRoot = URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + let settings = try String( + contentsOf: iosRoot.appendingPathComponent("Sources/Design/SettingsProTabSections.swift"), + encoding: .utf8) + let rootTabs = try String( + contentsOf: iosRoot.appendingPathComponent("Sources/RootTabs.swift"), + encoding: .utf8) + #expect(settings.contains("route: .systemAgent")) + #expect(settings + .contains("SettingsSystemAgentChatScreen(model: self.systemAgentChatStore.model(for: self.appModel))")) + #expect(rootTabs.contains(".onChange(of: self.appModel.openChatRequestID)")) + } + + private func makeModel( + accessState: IOSSystemAgentChatModel.AccessState = .ready, + recorder: RequestRecorder, + routeState: RouteState = RouteState()) -> IOSSystemAgentChatModel + { + IOSSystemAgentChatModel( + accessState: accessState, + routeIdentity: "gateway-a", + captureRoute: { _ in + IOSSystemAgentChatRouteLease( + route: nil, + request: { method, params, timeoutMs in + try await recorder.perform(method: method, params: params, timeoutMs: timeoutMs) + }, + isCurrent: { await routeState.isCurrent() }) + }) + } + + private static func start(_ model: IOSSystemAgentChatModel) async { + await model.startIfNeeded()?.value + } + + private static func waitUntil(_ condition: @escaping () async -> Bool) async { + for _ in 0..<100 { + if await condition() { return } + await Task.yield() + } + Issue.record("Timed out waiting for asynchronous condition") + } + + private static func reply( + _ reply: String, + action: String = "reply", + sensitive: Bool? = nil, + agentID: String? = nil) -> Data + { + var result: [String: Any] = [ + "sessionId": "system-session", + "reply": reply, + "action": action, + ] + if let sensitive { + result["sensitive"] = sensitive + } + if let agentID { + result["agentId"] = agentID + } + guard let data = try? JSONSerialization.data(withJSONObject: result) else { + preconditionFailure("System-agent reply fixture must encode") + } + return data + } + + private static func questionReply() -> Data { + let value: [String: Any] = [ + "sessionId": "system-session", + "reply": "Choose a connection", + "action": "reply", + "question": [ + "id": "connection", + "header": "Connection", + "question": "How should OpenClaw connect?", + "options": [ + [ + "label": "Use Tailscale", + "description": "Private network", + "recommended": true, + "reply": "tailscale", + ], + [ + "label": "Use LAN", + "description": "Local network", + "recommended": false, + "reply": "lan", + ], + ], + "isOther": false, + ], + ] + guard let data = try? JSONSerialization.data(withJSONObject: value) else { + preconditionFailure("System-agent question fixture must encode") + } + return data + } +} diff --git a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift index ba7a7d733924..4be360cfb577 100644 --- a/apps/ios/Tests/SwiftUIRenderSmokeTests.swift +++ b/apps/ios/Tests/SwiftUIRenderSmokeTests.swift @@ -102,6 +102,27 @@ struct SwiftUIRenderSmokeTests { } } + @Test @MainActor func `settings OpenClaw destination builds access gate across appearance and type size`() { + var windows: [UIWindow] = [] + defer { windows.forEach { $0.isHidden = true } } + + for scheme in [ColorScheme.light, ColorScheme.dark] { + for typeSize in [DynamicTypeSize.large, .accessibility2] { + let appModel = NodeAppModel() + let gatewayController = GatewayConnectionController(appModel: appModel, startDiscovery: false) + let root = SettingsProTab(directRoute: .systemAgent) + .environment(AppAppearanceModel()) + .environment(appModel) + .environment(appModel.voiceWake) + .environment(gatewayController) + .environment(\.dynamicTypeSize, typeSize) + .preferredColorScheme(scheme) + + windows.append(Self.host(root, size: CGSize(width: 393, height: 852))) + } + } + } + @Test @MainActor func `settings pro tab appearance row builds for all preferences`() throws { for preference in AppAppearancePreference.allCases { let suiteName = "OpenClawTests.appearance.\(preference.rawValue).\(UUID().uuidString)" diff --git a/docs/platforms/ios.md b/docs/platforms/ios.md index 6409bbe273b2..d4824733659c 100644 --- a/docs/platforms/ios.md +++ b/docs/platforms/ios.md @@ -19,6 +19,7 @@ Availability: iPhone app builds are distributed through Apple channels when enab - Keeps a small read-only offline cache of recent chat sessions and transcripts per paired gateway: cold opens paint the last known transcript immediately and refresh once the gateway responds, recent chats stay browsable while disconnected, and reset/forget purges the protected local cache. - Queues text messages sent while disconnected in a durable per-gateway outbox (up to 50): queued bubbles show in the transcript, flush in order on reconnect with idempotent retries, remain durable until canonical history confirms the send, retry with backoff before surfacing a retry/delete action, and expire instead of sending after 48 hours offline; reset/forget clears the queue with the cache. - Chat is the single text-and-voice surface. Chat actions can open the full Sessions screen without leaving Chat and can show or hide assistant reasoning and tool activity. Tap the microphone for draft dictation, open its menu to record a voice note, or use the inline Talk control for realtime voice; the Talk control animates from live microphone or playback level while listening or speaking. +- **Settings -> OpenClaw** opens a dedicated Gateway settings assistant when the operator connection has `operator.admin` and the Gateway supports `openclaw.chat`. Its setup conversation stays separate from ordinary Chat, redacts secret replies locally, and moves to Chat only after you tap **Open Chat**. - Speaks assistant messages on demand: long-press a message in Chat and choose **Listen**. The app plays supported gateway `tts.speak` clips with the configured TTS provider and falls back to on-device speech when gateway audio is unavailable or unplayable. Playback stops on session switch or backgrounding. ## Requirements