diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 6d7a397ab98..ee577a7bf03 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -1697,6 +1697,78 @@ "surface": "android", "id": "native.android.d7ea800cc2475013" }, + { + "kind": "conditional-branch", + "line": 1238, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Setup code expired. Scan a fresh setup QR.", + "surface": "android", + "id": "native.android.e44224ed4df1700f" + }, + { + "kind": "conditional-branch", + "line": 1242, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway password is required. Enter it again or edit this connection.", + "surface": "android", + "id": "native.android.511925b4321aae31" + }, + { + "kind": "conditional-branch", + "line": 1243, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway password is invalid. Re-enter it or reset this gateway connection.", + "surface": "android", + "id": "native.android.beeae935716d1ff4" + }, + { + "kind": "conditional-branch", + "line": 1244, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway token is required. Enter it again or edit this connection.", + "surface": "android", + "id": "native.android.0a5468f24a0ef9e8" + }, + { + "kind": "conditional-branch", + "line": 1246, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway requires this device identity. Re-authenticate or reset this gateway connection.", + "surface": "android", + "id": "native.android.11fbe9c58ebab116" + }, + { + "kind": "conditional-branch", + "line": 1250, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Saved authentication is invalid. Re-authenticate or reset this gateway connection.", + "surface": "android", + "id": "native.android.8a4c5b042f04d8ed" + }, + { + "kind": "conditional-branch", + "line": 1251, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway authentication is not configured. Edit this connection and try again.", + "surface": "android", + "id": "native.android.214b03b4cd2199b1" + }, + { + "kind": "conditional-branch", + "line": 1252, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "Gateway authentication needs review. Check gateway settings, then retry.", + "surface": "android", + "id": "native.android.1ccbb6f3911e7219" + }, + { + "kind": "conditional-branch", + "line": 1269, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/OnboardingFlow.kt", + "source": "$summary $it", + "surface": "android", + "id": "native.android.e9fa7abb92b3cc99" + }, { "kind": "ui-toast", "line": 1307, @@ -5233,6 +5305,22 @@ "surface": "apple", "id": "native.apple.6507fad2526215fc" }, + { + "kind": "ui-named-argument", + "line": 37, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "No promoted entries", + "surface": "apple", + "id": "native.apple.7e73f233a79a8007" + }, + { + "kind": "ui-named-argument", + "line": 38, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "Dreaming has not promoted durable memory entries yet.", + "surface": "apple", + "id": "native.apple.e20c6da13995b769" + }, { "kind": "ui-named-argument", "line": 40, @@ -5241,6 +5329,22 @@ "surface": "apple", "id": "native.apple.f37dc8d03ad70555" }, + { + "kind": "ui-named-argument", + "line": 42, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "No signal entries", + "surface": "apple", + "id": "native.apple.2802710c41f25f87" + }, + { + "kind": "ui-named-argument", + "line": 43, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "No recent recall, daily, grounded, or phase signals were reported.", + "surface": "apple", + "id": "native.apple.2665e709446f3510" + }, { "kind": "ui-named-argument", "line": 45, @@ -5249,6 +5353,22 @@ "surface": "apple", "id": "native.apple.188e21c57debe55d" }, + { + "kind": "ui-named-argument", + "line": 47, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "No short-term entries", + "surface": "apple", + "id": "native.apple.da521f32f4d1b56d" + }, + { + "kind": "ui-named-argument", + "line": 48, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "The short-term dreaming store is empty.", + "surface": "apple", + "id": "native.apple.3533db79b1ce15b2" + }, { "kind": "ui-named-argument", "line": 66, @@ -5353,6 +5473,14 @@ "surface": "apple", "id": "native.apple.825726d0205d5bd4" }, + { + "kind": "ui-named-argument", + "line": 245, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "The diary is present, but it does not contain dated Dream Diary blocks.", + "surface": "apple", + "id": "native.apple.ab19be5a9d08b82e" + }, { "kind": "conditional-branch", "line": 252, @@ -5425,6 +5553,14 @@ "surface": "apple", "id": "native.apple.21d92f01c5cc2412" }, + { + "kind": "ui-named-argument", + "line": 343, + "path": "apps/ios/Sources/Design/AgentProDreamingDestination.swift", + "source": "Connect a gateway to load dreaming entries.", + "surface": "apple", + "id": "native.apple.f0df95f0790fe675" + }, { "kind": "ui-call", "line": 378, @@ -5729,17 +5865,9 @@ "surface": "apple", "id": "native.apple.8658a6a0ee787fc3" }, - { - "kind": "ui-modifier", - "line": 40, - "path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift", - "source": "Agents", - "surface": "apple", - "id": "native.apple.ac789046a06d6afa" - }, { "kind": "ui-named-argument", - "line": 51, + "line": 52, "path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift", "source": "Skills", "surface": "apple", @@ -5747,7 +5875,7 @@ }, { "kind": "ui-named-argument", - "line": 96, + "line": 97, "path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift", "source": "Cron Jobs", "surface": "apple", @@ -5755,7 +5883,7 @@ }, { "kind": "ui-named-argument", - "line": 125, + "line": 126, "path": "apps/ios/Sources/Design/AgentProTab+Destinations.swift", "source": "Usage", "surface": "apple", @@ -5763,11 +5891,35 @@ }, { "kind": "conditional-branch", - "line": 42, + "line": 43, "path": "apps/ios/Sources/Design/AgentProTab+GatewayData.swift", - "source": "1 running", + "source": "Online", "surface": "apple", - "id": "native.apple.5ace4af742f70cb5" + "id": "native.apple.5fbed24f057edea8" + }, + { + "kind": "conditional-branch", + "line": 43, + "path": "apps/ios/Sources/Design/AgentProTab+GatewayData.swift", + "source": "Ready", + "surface": "apple", + "id": "native.apple.f1958c5f0d880a4a" + }, + { + "kind": "conditional-branch", + "line": 44, + "path": "apps/ios/Sources/Design/AgentProTab+GatewayData.swift", + "source": "Not selected", + "surface": "apple", + "id": "native.apple.056ac76b75d6795d" + }, + { + "kind": "conditional-branch", + "line": 44, + "path": "apps/ios/Sources/Design/AgentProTab+GatewayData.swift", + "source": "Selected", + "surface": "apple", + "id": "native.apple.c29b4a63fb797a83" }, { "kind": "ui-named-argument", @@ -5779,47 +5931,39 @@ }, { "kind": "ui-named-argument", - "line": 23, + "line": 24, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Search agents", "surface": "apple", "id": "native.apple.5ee1fa5a66b2d846" }, - { - "kind": "conditional-branch", - "line": 31, - "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Refresh agents", - "surface": "apple", - "id": "native.apple.30980781b2db0bfa" - }, - { - "kind": "conditional-branch", - "line": 31, - "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Refreshing agents", - "surface": "apple", - "id": "native.apple.d94eb7a2ac60ce53" - }, { "kind": "ui-modifier", - "line": 67, + "line": 57, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Opens Settings / Gateway", "surface": "apple", "id": "native.apple.9c45abb65d0ba8f3" }, { - "kind": "ui-named-argument", - "line": 104, + "kind": "ui-call", + "line": 65, + "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", + "source": "Agent status", + "surface": "apple", + "id": "native.apple.23ed9b5b0d1721d3" + }, + { + "kind": "ui-modifier", + "line": 86, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Clear filters", "surface": "apple", - "id": "native.apple.d385e7500ccb2328" + "id": "native.apple.5cc5151b7946d32e" }, { "kind": "ui-named-argument", - "line": 142, + "line": 118, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Live Operations", "surface": "apple", @@ -5827,7 +5971,7 @@ }, { "kind": "ui-named-argument", - "line": 146, + "line": 122, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Skills", "surface": "apple", @@ -5835,7 +5979,7 @@ }, { "kind": "ui-named-argument", - "line": 153, + "line": 129, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Instances", "surface": "apple", @@ -5843,7 +5987,7 @@ }, { "kind": "ui-named-argument", - "line": 160, + "line": 136, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Cron", "surface": "apple", @@ -5851,7 +5995,7 @@ }, { "kind": "ui-named-argument", - "line": 167, + "line": 143, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Usage", "surface": "apple", @@ -5859,7 +6003,7 @@ }, { "kind": "ui-named-argument", - "line": 191, + "line": 167, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Dreaming", "surface": "apple", @@ -5867,47 +6011,31 @@ }, { "kind": "ui-named-argument", - "line": 205, + "line": 181, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Scheduled Work", "surface": "apple", "id": "native.apple.4999264d74844f21" }, { - "kind": "ui-named-argument", - "line": 277, + "kind": "conditional-branch", + "line": 262, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Sessions", + "source": "Selected agent", "surface": "apple", - "id": "native.apple.cc1da225b8d4ffd7" - }, - { - "kind": "ui-named-argument", - "line": 281, - "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Runtime", - "surface": "apple", - "id": "native.apple.1cf141f75315e271" + "id": "native.apple.13245a5075869f14" }, { "kind": "conditional-branch", - "line": 302, + "line": 262, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Default agent", + "source": "Selects this agent", "surface": "apple", - "id": "native.apple.265d94a831fdf29d" + "id": "native.apple.ae9101fc699ad5f8" }, { "kind": "conditional-branch", - "line": 302, - "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", - "source": "Set default agent", - "surface": "apple", - "id": "native.apple.aa0edf41d65eb243" - }, - { - "kind": "conditional-branch", - "line": 478, + "line": 403, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Cron unavailable", "surface": "apple", @@ -5915,7 +6043,7 @@ }, { "kind": "conditional-branch", - "line": 478, + "line": 403, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "No scheduled jobs", "surface": "apple", @@ -5923,7 +6051,7 @@ }, { "kind": "conditional-branch", - "line": 481, + "line": 406, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Connect a gateway to load scheduled work.", "surface": "apple", @@ -5931,7 +6059,7 @@ }, { "kind": "conditional-branch", - "line": 481, + "line": 406, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "The gateway has no visible cron jobs.", "surface": "apple", @@ -5939,7 +6067,7 @@ }, { "kind": "conditional-branch", - "line": 618, + "line": 526, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Loading skill status.", "surface": "apple", @@ -5947,7 +6075,7 @@ }, { "kind": "conditional-branch", - "line": 618, + "line": 526, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Skill status is available from the gateway.", "surface": "apple", @@ -5955,7 +6083,7 @@ }, { "kind": "conditional-branch", - "line": 640, + "line": 548, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Instance presence is available.", "surface": "apple", @@ -5963,7 +6091,7 @@ }, { "kind": "conditional-branch", - "line": 640, + "line": 548, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Loading instance presence.", "surface": "apple", @@ -5971,7 +6099,7 @@ }, { "kind": "conditional-branch", - "line": 659, + "line": 567, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "\\(cronStatus.jobs)", "surface": "apple", @@ -5979,7 +6107,7 @@ }, { "kind": "conditional-branch", - "line": 665, + "line": 573, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Cron status is available.", "surface": "apple", @@ -5987,7 +6115,7 @@ }, { "kind": "conditional-branch", - "line": 665, + "line": 573, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Loading cron status.", "surface": "apple", @@ -5995,7 +6123,7 @@ }, { "kind": "conditional-branch", - "line": 670, + "line": 578, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Scheduler disabled", "surface": "apple", @@ -6003,7 +6131,7 @@ }, { "kind": "conditional-branch", - "line": 670, + "line": 578, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Scheduler enabled", "surface": "apple", @@ -6011,7 +6139,7 @@ }, { "kind": "conditional-branch", - "line": 695, + "line": 603, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Loading recent usage.", "surface": "apple", @@ -6019,7 +6147,7 @@ }, { "kind": "conditional-branch", - "line": 695, + "line": 603, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Recent usage is available.", "surface": "apple", @@ -6027,7 +6155,7 @@ }, { "kind": "conditional-branch", - "line": 714, + "line": 622, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Background memory status is available.", "surface": "apple", @@ -6035,7 +6163,7 @@ }, { "kind": "conditional-branch", - "line": 714, + "line": 622, "path": "apps/ios/Sources/Design/AgentProTab+Overview.swift", "source": "Loading dreaming status.", "surface": "apple", @@ -6385,6 +6513,14 @@ "surface": "apple", "id": "native.apple.51f8cdef7b566774" }, + { + "kind": "ui-named-argument", + "line": 46, + "path": "apps/ios/Sources/Design/AgentProTab+Usage.swift", + "source": "The gateway returned totals without daily session cost rows.", + "surface": "apple", + "id": "native.apple.85a6c5af01f07a8b" + }, { "kind": "ui-call", "line": 69, @@ -6395,7 +6531,7 @@ }, { "kind": "conditional-branch", - "line": 60, + "line": 58, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Enabled", "surface": "apple", @@ -6403,7 +6539,7 @@ }, { "kind": "conditional-branch", - "line": 61, + "line": 59, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Off", "surface": "apple", @@ -6411,7 +6547,7 @@ }, { "kind": "conditional-branch", - "line": 62, + "line": 60, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Setup", "surface": "apple", @@ -6419,7 +6555,7 @@ }, { "kind": "conditional-branch", - "line": 63, + "line": 61, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Blocked", "surface": "apple", @@ -6427,7 +6563,7 @@ }, { "kind": "conditional-branch", - "line": 79, + "line": 77, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "All", "surface": "apple", @@ -6435,7 +6571,7 @@ }, { "kind": "conditional-branch", - "line": 100, + "line": 78, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Online", "surface": "apple", @@ -6443,7 +6579,7 @@ }, { "kind": "conditional-branch", - "line": 101, + "line": 79, "path": "apps/ios/Sources/Design/AgentProTab.swift", "source": "Ready", "surface": "apple", @@ -6467,7 +6603,7 @@ }, { "kind": "ui-modifier", - "line": 212, + "line": 206, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Opens Settings / Gateway", "surface": "apple", @@ -6475,7 +6611,7 @@ }, { "kind": "conditional-branch", - "line": 265, + "line": 251, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Connected", "surface": "apple", @@ -6483,7 +6619,7 @@ }, { "kind": "conditional-branch", - "line": 265, + "line": 251, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Unavailable", "surface": "apple", @@ -6491,7 +6627,7 @@ }, { "kind": "conditional-branch", - "line": 276, + "line": 262, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Connect to a gateway", "surface": "apple", @@ -6499,7 +6635,7 @@ }, { "kind": "conditional-branch", - "line": 276, + "line": 262, "path": "apps/ios/Sources/Design/ChatProTab.swift", "source": "Message \\(self.agentDisplayName)...", "surface": "apple", @@ -6507,7 +6643,7 @@ }, { "kind": "ui-call", - "line": 126, + "line": 99, "path": "apps/ios/Sources/Design/CommandCenterSupport.swift", "source": "View More", "surface": "apple", @@ -6515,23 +6651,23 @@ }, { "kind": "ui-modifier", - "line": 128, + "line": 127, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", - "source": "Gateway \\(self.gatewayStateText)", + "source": "Gateway settings", "surface": "apple", - "id": "native.apple.1dd03a218068f08e" + "id": "native.apple.65fb4d9ce6e5bbda" }, { "kind": "ui-modifier", - "line": 129, + "line": 128, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", - "source": "Opens Settings / Gateway", + "source": "Opens gateway settings", "surface": "apple", - "id": "native.apple.897981356da91ac6" + "id": "native.apple.4eaa7637d355c07c" }, { "kind": "ui-named-argument", - "line": 154, + "line": 152, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Gateway", "surface": "apple", @@ -6539,31 +6675,15 @@ }, { "kind": "ui-named-argument", - "line": 162, + "line": 157, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Connection", "surface": "apple", "id": "native.apple.88d022e6c15a3dc3" }, - { - "kind": "conditional-branch", - "line": 163, - "path": "apps/ios/Sources/Design/CommandCenterTab.swift", - "source": "Offline", - "surface": "apple", - "id": "native.apple.89989b6d557090e8" - }, - { - "kind": "conditional-branch", - "line": 163, - "path": "apps/ios/Sources/Design/CommandCenterTab.swift", - "source": "Online", - "surface": "apple", - "id": "native.apple.e01aac63fda4c147" - }, { "kind": "ui-named-argument", - "line": 168, + "line": 163, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Address", "surface": "apple", @@ -6571,7 +6691,7 @@ }, { "kind": "ui-named-argument", - "line": 174, + "line": 169, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Agents", "surface": "apple", @@ -6579,7 +6699,7 @@ }, { "kind": "ui-named-argument", - "line": 219, + "line": 203, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Agent session", "surface": "apple", @@ -6587,15 +6707,47 @@ }, { "kind": "ui-named-argument", - "line": 237, + "line": 218, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Recent sessions", "surface": "apple", "id": "native.apple.27f775de30b37ff8" }, + { + "kind": "conditional-branch", + "line": 277, + "path": "apps/ios/Sources/Design/CommandCenterTab.swift", + "source": "Online", + "surface": "apple", + "id": "native.apple.e01aac63fda4c147" + }, + { + "kind": "conditional-branch", + "line": 279, + "path": "apps/ios/Sources/Design/CommandCenterTab.swift", + "source": "Connecting", + "surface": "apple", + "id": "native.apple.a857bc74fd053781" + }, + { + "kind": "conditional-branch", + "line": 281, + "path": "apps/ios/Sources/Design/CommandCenterTab.swift", + "source": "Attention", + "surface": "apple", + "id": "native.apple.a36675969878b0d8" + }, + { + "kind": "conditional-branch", + "line": 283, + "path": "apps/ios/Sources/Design/CommandCenterTab.swift", + "source": "Offline", + "surface": "apple", + "id": "native.apple.89989b6d557090e8" + }, { "kind": "ui-call", - "line": 673, + "line": 635, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Sessions", "surface": "apple", @@ -6603,7 +6755,7 @@ }, { "kind": "ui-named-argument", - "line": 702, + "line": 664, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Sessions unavailable", "surface": "apple", @@ -6611,7 +6763,7 @@ }, { "kind": "conditional-branch", - "line": 712, + "line": 674, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Connect to the gateway.", "surface": "apple", @@ -6619,7 +6771,7 @@ }, { "kind": "conditional-branch", - "line": 712, + "line": 674, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Start a chat and it will appear here.", "surface": "apple", @@ -6627,7 +6779,7 @@ }, { "kind": "conditional-branch", - "line": 739, + "line": 701, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "Gateway offline", "surface": "apple", @@ -6635,7 +6787,7 @@ }, { "kind": "conditional-branch", - "line": 739, + "line": 701, "path": "apps/ios/Sources/Design/CommandCenterTab.swift", "source": "No recent sessions", "surface": "apple", @@ -6681,6 +6833,14 @@ "surface": "apple", "id": "native.apple.4813254a14ae910f" }, + { + "kind": "ui-named-argument", + "line": 69, + "path": "apps/ios/Sources/Design/IPadActivityScreen.swift", + "source": "Refresh", + "surface": "apple", + "id": "native.apple.f7b3242d69bc344b" + }, { "kind": "ui-named-argument", "line": 77, @@ -6713,6 +6873,14 @@ "surface": "apple", "id": "native.apple.2360d25fe6ae9946" }, + { + "kind": "ui-named-argument", + "line": 111, + "path": "apps/ios/Sources/Design/IPadActivityScreen.swift", + "source": "Fetching recent activity from the gateway.", + "surface": "apple", + "id": "native.apple.48655128d0a34e44" + }, { "kind": "ui-named-argument", "line": 120, @@ -6753,6 +6921,14 @@ "surface": "apple", "id": "native.apple.e3586d8a603f67e0" }, + { + "kind": "ui-named-argument", + "line": 147, + "path": "apps/ios/Sources/Design/IPadActivityScreen.swift", + "source": "Open", + "surface": "apple", + "id": "native.apple.fb6493b448a3f62a" + }, { "kind": "conditional-branch", "line": 11, @@ -6771,7 +6947,7 @@ }, { "kind": "ui-modifier", - "line": 62, + "line": 63, "path": "apps/ios/Sources/Design/IPadSidebarScreenChrome.swift", "source": "Opens Settings / Gateway", "surface": "apple", @@ -6913,6 +7089,14 @@ "surface": "apple", "id": "native.apple.7df7629c97f9ce5c" }, + { + "kind": "ui-named-argument", + "line": 396, + "path": "apps/ios/Sources/Design/IPadSkillWorkshopScreen.swift", + "source": "Return to the queue and choose another proposal.", + "surface": "apple", + "id": "native.apple.4346e78325e97c0b" + }, { "kind": "ui-call", "line": 433, @@ -6961,6 +7145,14 @@ "surface": "apple", "id": "native.apple.16e5b5a92891e431" }, + { + "kind": "ui-named-argument", + "line": 891, + "path": "apps/ios/Sources/Design/IPadSkillWorkshopScreen.swift", + "source": "Matching proposals appear here after gateway refresh.", + "surface": "apple", + "id": "native.apple.d551631fd59d66ba" + }, { "kind": "ui-modifier", "line": 973, @@ -7225,6 +7417,14 @@ "surface": "apple", "id": "native.apple.32c4f348c9f4e313" }, + { + "kind": "ui-named-argument", + "line": 921, + "path": "apps/ios/Sources/Design/IPadWorkboardScreen.swift", + "source": "Cards moved into this lane appear here.", + "surface": "apple", + "id": "native.apple.30c47306610cb517" + }, { "kind": "ui-modifier", "line": 1100, @@ -7361,6 +7561,30 @@ "surface": "apple", "id": "native.apple.e3576faef11c7eba" }, + { + "kind": "conditional-branch", + "line": 34, + "path": "apps/ios/Sources/Design/OpenClawBrand.swift", + "source": "Matches the system appearance.", + "surface": "apple", + "id": "native.apple.00166633e917a91f" + }, + { + "kind": "conditional-branch", + "line": 35, + "path": "apps/ios/Sources/Design/OpenClawBrand.swift", + "source": "Always uses light appearance.", + "surface": "apple", + "id": "native.apple.0ee9dc6143fd4c22" + }, + { + "kind": "conditional-branch", + "line": 36, + "path": "apps/ios/Sources/Design/OpenClawBrand.swift", + "source": "Always uses dark appearance.", + "surface": "apple", + "id": "native.apple.23af5fc64d16219d" + }, { "kind": "ui-named-argument", "line": 34, @@ -7379,7 +7603,7 @@ }, { "kind": "ui-modifier", - "line": 59, + "line": 60, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "Opens Settings / Gateway", "surface": "apple", @@ -7387,7 +7611,7 @@ }, { "kind": "ui-named-argument", - "line": 69, + "line": 70, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "Docs Home", "surface": "apple", @@ -7395,7 +7619,15 @@ }, { "kind": "ui-named-argument", - "line": 75, + "line": 71, + "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", + "source": "Browse the current OpenClaw reference.", + "surface": "apple", + "id": "native.apple.56935c5384889b8e" + }, + { + "kind": "ui-named-argument", + "line": 76, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "Gateway", "surface": "apple", @@ -7403,15 +7635,31 @@ }, { "kind": "ui-named-argument", - "line": 81, + "line": 77, + "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", + "source": "Connection, auth, and diagnostics.", + "surface": "apple", + "id": "native.apple.610af6c97ab9f33a" + }, + { + "kind": "ui-named-argument", + "line": 82, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "Pairing", "surface": "apple", "id": "native.apple.a25d827f6577a87f" }, + { + "kind": "ui-named-argument", + "line": 83, + "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", + "source": "Mobile setup codes, QR, and node approval.", + "surface": "apple", + "id": "native.apple.5217a044d98947d5" + }, { "kind": "ui-call", - "line": 93, + "line": 94, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "Version", "surface": "apple", @@ -7419,15 +7667,23 @@ }, { "kind": "ui-call", - "line": 97, + "line": 98, "path": "apps/ios/Sources/Design/OpenClawDocsScreen.swift", "source": "v\\(DeviceInfoHelper.openClawVersionString())", "surface": "apple", "id": "native.apple.c589f3ed9e2505cc" }, + { + "kind": "ui-call", + "line": 374, + "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", + "source": "Request ID: \\(value)", + "surface": "apple", + "id": "native.apple.f36a9ff70377109b" + }, { "kind": "ui-modifier", - "line": 402, + "line": 507, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "OpenClaw", "surface": "apple", @@ -7435,7 +7691,7 @@ }, { "kind": "ui-modifier", - "line": 465, + "line": 545, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Gateway \\(self.title)", "surface": "apple", @@ -7443,7 +7699,7 @@ }, { "kind": "conditional-branch", - "line": 470, + "line": 550, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Online", "surface": "apple", @@ -7451,7 +7707,7 @@ }, { "kind": "conditional-branch", - "line": 472, + "line": 552, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Connecting", "surface": "apple", @@ -7459,7 +7715,7 @@ }, { "kind": "conditional-branch", - "line": 474, + "line": 554, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Attention", "surface": "apple", @@ -7467,7 +7723,7 @@ }, { "kind": "conditional-branch", - "line": 476, + "line": 556, "path": "apps/ios/Sources/Design/OpenClawProComponents.swift", "source": "Offline", "surface": "apple", @@ -7483,7 +7739,7 @@ }, { "kind": "ui-modifier", - "line": 117, + "line": 77, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Gateway \\(self.gatewayStateText)", "surface": "apple", @@ -7491,15 +7747,55 @@ }, { "kind": "ui-modifier", - "line": 118, + "line": 78, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Opens Settings / Gateway", "surface": "apple", "id": "native.apple.597471e86dda3d38" }, + { + "kind": "ui-named-argument", + "line": 143, + "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", + "source": "Overview", + "surface": "apple", + "id": "native.apple.a03e961ab14ba060" + }, + { + "kind": "ui-named-argument", + "line": 167, + "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", + "source": "Instances", + "surface": "apple", + "id": "native.apple.feb28c9cba676ba2" + }, + { + "kind": "ui-named-argument", + "line": 177, + "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", + "source": "Dreaming", + "surface": "apple", + "id": "native.apple.614c9b40919fb103" + }, + { + "kind": "ui-named-argument", + "line": 183, + "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", + "source": "Usage", + "surface": "apple", + "id": "native.apple.e2b5c2d7c5d461a3" + }, + { + "kind": "ui-named-argument", + "line": 189, + "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", + "source": "Cron Jobs", + "surface": "apple", + "id": "native.apple.9eb33a11fb81796b" + }, { "kind": "conditional-branch", - "line": 293, + "line": 249, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Online", "surface": "apple", @@ -7507,7 +7803,7 @@ }, { "kind": "conditional-branch", - "line": 294, + "line": 250, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Connecting", "surface": "apple", @@ -7515,7 +7811,7 @@ }, { "kind": "conditional-branch", - "line": 295, + "line": 251, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Attention", "surface": "apple", @@ -7523,44 +7819,12 @@ }, { "kind": "conditional-branch", - "line": 296, + "line": 252, "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", "source": "Offline", "surface": "apple", "id": "native.apple.1c931bcd1cd20334" }, - { - "kind": "conditional-branch", - "line": 315, - "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", - "source": "Manage", - "surface": "apple", - "id": "native.apple.b9601a68d7077978" - }, - { - "kind": "conditional-branch", - "line": 317, - "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", - "source": "Details", - "surface": "apple", - "id": "native.apple.535f65f246368ede" - }, - { - "kind": "conditional-branch", - "line": 319, - "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", - "source": "Fix", - "surface": "apple", - "id": "native.apple.19c007068a9f0939" - }, - { - "kind": "conditional-branch", - "line": 321, - "path": "apps/ios/Sources/Design/RootTabsPhoneControlHub.swift", - "source": "Connect", - "surface": "apple", - "id": "native.apple.71e9f5c2f74d4a64" - }, { "kind": "ui-call", "line": 38, @@ -7577,6 +7841,14 @@ "surface": "apple", "id": "native.apple.aacfc3d31109d5ab" }, + { + "kind": "ui-named-argument", + "line": 83, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Fetching installed channels, accounts, and routing status from the gateway.", + "surface": "apple", + "id": "native.apple.373913573fe8474a" + }, { "kind": "ui-call", "line": 396, @@ -7633,6 +7905,14 @@ "surface": "apple", "id": "native.apple.39ec8a10330f9bb0" }, + { + "kind": "ui-named-argument", + "line": 581, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Checking installed channel clients and account state.", + "surface": "apple", + "id": "native.apple.2183ebee2edb4b04" + }, { "kind": "ui-call", "line": 586, @@ -7649,6 +7929,14 @@ "surface": "apple", "id": "native.apple.5fbbe373a4d427fa" }, + { + "kind": "ui-named-argument", + "line": 591, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Refresh Channels", + "surface": "apple", + "id": "native.apple.9918aa58bf346645" + }, { "kind": "ui-named-argument", "line": 595, @@ -7657,6 +7945,14 @@ "surface": "apple", "id": "native.apple.de33101d0c049a5f" }, + { + "kind": "ui-named-argument", + "line": 596, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Install or enable channel plugins on the gateway, then refresh.", + "surface": "apple", + "id": "native.apple.afee604367e1caee" + }, { "kind": "ui-call", "line": 601, @@ -7673,6 +7969,14 @@ "surface": "apple", "id": "native.apple.aa97425de5b33828" }, + { + "kind": "ui-named-argument", + "line": 605, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Gateway returned an unexpected channel status response.", + "surface": "apple", + "id": "native.apple.889115c46972d544" + }, { "kind": "ui-call", "line": 610, @@ -7689,6 +7993,14 @@ "surface": "apple", "id": "native.apple.59c349b1b581354f" }, + { + "kind": "ui-named-argument", + "line": 614, + "path": "apps/ios/Sources/Design/SettingsChannelsDestination.swift", + "source": "Connect to the gateway to load installed channels, accounts, and routing status.", + "surface": "apple", + "id": "native.apple.6308264d116d5be6" + }, { "kind": "ui-modifier", "line": 220, @@ -7809,6 +8121,14 @@ "surface": "apple", "id": "native.apple.2a54915eabe91c06" }, + { + "kind": "ui-named-argument", + "line": 66, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "Approval and event alert channel", + "surface": "apple", + "id": "native.apple.5f795158b2f43ea8" + }, { "kind": "ui-named-argument", "line": 72, @@ -7817,6 +8137,14 @@ "surface": "apple", "id": "native.apple.ab6c665aea1a9525" }, + { + "kind": "ui-named-argument", + "line": 73, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "Live foreground capture state", + "surface": "apple", + "id": "native.apple.d5c33a7a3b374f89" + }, { "kind": "ui-named-argument", "line": 79, @@ -7985,6 +8313,46 @@ "surface": "apple", "id": "native.apple.7774b67d295ea77d" }, + { + "kind": "conditional-branch", + "line": 837, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "Checking iOS notification permission.", + "surface": "apple", + "id": "native.apple.2829150c12160b55" + }, + { + "kind": "conditional-branch", + "line": 839, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "OpenClaw can show approval prompts and event alerts when the app is not active.", + "surface": "apple", + "id": "native.apple.ea2016fd75ff79c9" + }, + { + "kind": "conditional-branch", + "line": 841, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "Notifications have been denied. Enable them in iOS Settings.", + "surface": "apple", + "id": "native.apple.408e00d224b5a5ac" + }, + { + "kind": "conditional-branch", + "line": 843, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "Enable notifications to receive approval prompts and event alerts outside the app.", + "surface": "apple", + "id": "native.apple.33add8a7917bad7e" + }, + { + "kind": "conditional-branch", + "line": 845, + "path": "apps/ios/Sources/Design/SettingsProTabActions.swift", + "source": "OpenClaw cannot determine the current notification permission state.", + "surface": "apple", + "id": "native.apple.b525ff6cead949f4" + }, { "kind": "ui-named-argument", "line": 7, @@ -7995,55 +8363,31 @@ }, { "kind": "ui-named-argument", - "line": 8, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Gateway, permissions, voice, and device controls.", - "surface": "apple", - "id": "native.apple.88ba4b3e5ade8fb1" - }, - { - "kind": "ui-named-argument", - "line": 24, + "line": 23, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Appearance", "surface": "apple", "id": "native.apple.5f60795231b82614" }, { - "kind": "ui-named-argument", - "line": 55, + "kind": "conditional-branch", + "line": 86, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Address", + "source": "1 agent", "surface": "apple", - "id": "native.apple.36fd1dbc0ee32e2f" + "id": "native.apple.0b50dca4526336be" + }, + { + "kind": "conditional-branch", + "line": 86, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "\\(agentCount) agents", + "surface": "apple", + "id": "native.apple.4e579fbdce03c39a" }, { "kind": "ui-named-argument", - "line": 57, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Server", - "surface": "apple", - "id": "native.apple.84194207a8c86a10" - }, - { - "kind": "ui-named-argument", - "line": 59, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Agents", - "surface": "apple", - "id": "native.apple.b12dc9f5cb8ba702" - }, - { - "kind": "ui-call", - "line": 76, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Connection", - "surface": "apple", - "id": "native.apple.70eca66c79335a6e" - }, - { - "kind": "ui-named-argument", - "line": 110, + "line": 93, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Reconnect", "surface": "apple", @@ -8051,7 +8395,7 @@ }, { "kind": "ui-named-argument", - "line": 120, + "line": 103, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Diagnose", "surface": "apple", @@ -8059,7 +8403,7 @@ }, { "kind": "ui-named-argument", - "line": 141, + "line": 127, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Permissions", "surface": "apple", @@ -8067,15 +8411,31 @@ }, { "kind": "ui-named-argument", - "line": 146, + "line": 133, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Channels / Integrations", + "source": "Channels", "surface": "apple", - "id": "native.apple.a5310ad86db49fb6" + "id": "native.apple.8a159fab30307884" }, { "kind": "ui-named-argument", - "line": 156, + "line": 134, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Connected services and message routing", + "surface": "apple", + "id": "native.apple.7698fd0a8047e31e" + }, + { + "kind": "ui-named-argument", + "line": 147, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Device", + "surface": "apple", + "id": "native.apple.2282000b1234eae3" + }, + { + "kind": "ui-named-argument", + "line": 152, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Diagnostics", "surface": "apple", @@ -8083,7 +8443,7 @@ }, { "kind": "ui-named-argument", - "line": 171, + "line": 170, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "About", "surface": "apple", @@ -8091,7 +8451,7 @@ }, { "kind": "ui-named-argument", - "line": 275, + "line": 278, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Gateway", "surface": "apple", @@ -8099,15 +8459,39 @@ }, { "kind": "ui-call", - "line": 285, + "line": 284, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Address", + "surface": "apple", + "id": "native.apple.800b6502b5b49941" + }, + { + "kind": "ui-call", + "line": 286, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Server", + "surface": "apple", + "id": "native.apple.fde8b41db7ec49f3" + }, + { + "kind": "ui-call", + "line": 288, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Discovered", "surface": "apple", "id": "native.apple.624f9dc99e8f9ff4" }, + { + "kind": "ui-call", + "line": 292, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Agents", + "surface": "apple", + "id": "native.apple.8e3656d85f092897" + }, { "kind": "ui-named-argument", - "line": 310, + "line": 313, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Approvals", "surface": "apple", @@ -8115,7 +8499,7 @@ }, { "kind": "conditional-branch", - "line": 313, + "line": 316, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "No gateway actions are waiting for review.", "surface": "apple", @@ -8123,7 +8507,7 @@ }, { "kind": "conditional-branch", - "line": 313, + "line": 316, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Review the pending gateway action.", "surface": "apple", @@ -8131,7 +8515,7 @@ }, { "kind": "conditional-branch", - "line": 317, + "line": 320, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "1 waiting", "surface": "apple", @@ -8139,7 +8523,7 @@ }, { "kind": "ui-call", - "line": 335, + "line": 338, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Notifications are off", "surface": "apple", @@ -8147,7 +8531,7 @@ }, { "kind": "ui-call-multiline", - "line": 337, + "line": 340, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Enable Notifications to receive approval notifications while OpenClaw is not open.", "surface": "apple", @@ -8155,7 +8539,7 @@ }, { "kind": "ui-call", - "line": 351, + "line": 354, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Open Notifications", "surface": "apple", @@ -8163,7 +8547,7 @@ }, { "kind": "ui-call", - "line": 385, + "line": 388, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Allow", "surface": "apple", @@ -8171,7 +8555,7 @@ }, { "kind": "ui-call", - "line": 405, + "line": 408, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Deny", "surface": "apple", @@ -8179,7 +8563,7 @@ }, { "kind": "ui-call", - "line": 417, + "line": 420, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "No approvals waiting", "surface": "apple", @@ -8187,7 +8571,7 @@ }, { "kind": "ui-named-argument", - "line": 435, + "line": 438, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Camera", "surface": "apple", @@ -8195,7 +8579,15 @@ }, { "kind": "ui-named-argument", - "line": 443, + "line": 439, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Allow the gateway to request photos or video while OpenClaw is foregrounded.", + "surface": "apple", + "id": "native.apple.68d3c5dbdb054a8b" + }, + { + "kind": "ui-named-argument", + "line": 446, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Keep Awake", "surface": "apple", @@ -8203,7 +8595,15 @@ }, { "kind": "ui-named-argument", - "line": 455, + "line": 447, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Keep the screen awake while OpenClaw is open.", + "surface": "apple", + "id": "native.apple.1b32ce3678bba852" + }, + { + "kind": "ui-named-argument", + "line": 458, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Voice & Talk", "surface": "apple", @@ -8211,7 +8611,7 @@ }, { "kind": "ui-named-argument", - "line": 470, + "line": 473, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Health Check", "surface": "apple", @@ -8219,7 +8619,15 @@ }, { "kind": "ui-named-argument", - "line": 477, + "line": 474, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Run app, permission, and gateway-adjacent checks without editing setup.", + "surface": "apple", + "id": "native.apple.076a83f6eed9161a" + }, + { + "kind": "ui-named-argument", + "line": 480, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Run Diagnostics", "surface": "apple", @@ -8227,7 +8635,7 @@ }, { "kind": "ui-call", - "line": 494, + "line": 497, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "App", "surface": "apple", @@ -8235,7 +8643,7 @@ }, { "kind": "ui-named-argument", - "line": 507, + "line": 510, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Privacy", "surface": "apple", @@ -8243,7 +8651,15 @@ }, { "kind": "ui-named-argument", - "line": 514, + "line": 511, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Control what device context OpenClaw can expose to the gateway.", + "surface": "apple", + "id": "native.apple.6d2f325ea39a5ee5" + }, + { + "kind": "ui-named-argument", + "line": 517, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Camera Access", "surface": "apple", @@ -8251,7 +8667,15 @@ }, { "kind": "ui-named-argument", - "line": 522, + "line": 518, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Disable to block camera capture requests from the gateway.", + "surface": "apple", + "id": "native.apple.3a6fd1b30501990f" + }, + { + "kind": "ui-named-argument", + "line": 525, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Background Listening", "surface": "apple", @@ -8259,7 +8683,15 @@ }, { "kind": "ui-named-argument", - "line": 534, + "line": 526, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "Allow active Talk sessions to continue while the app is backgrounded.", + "surface": "apple", + "id": "native.apple.3b12afefbeaca766" + }, + { + "kind": "ui-named-argument", + "line": 537, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Notifications", "surface": "apple", @@ -8267,15 +8699,23 @@ }, { "kind": "ui-named-argument", - "line": 580, + "line": 583, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "OpenClaw", "surface": "apple", "id": "native.apple.3766f0978df083ae" }, + { + "kind": "ui-named-argument", + "line": 584, + "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", + "source": "iOS companion app", + "surface": "apple", + "id": "native.apple.9df840cb4e010710" + }, { "kind": "ui-call", - "line": 586, + "line": 589, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Version", "surface": "apple", @@ -8283,15 +8723,7 @@ }, { "kind": "ui-call", - "line": 588, - "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", - "source": "Device", - "surface": "apple", - "id": "native.apple.f0a16e985312895b" - }, - { - "kind": "ui-call", - "line": 590, + "line": 593, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Platform", "surface": "apple", @@ -8299,7 +8731,7 @@ }, { "kind": "ui-call", - "line": 592, + "line": 595, "path": "apps/ios/Sources/Design/SettingsProTabSections.swift", "source": "Model", "surface": "apple", @@ -8753,6 +9185,14 @@ "surface": "apple", "id": "native.apple.f79541e88a1434df" }, + { + "kind": "ui-named-argument", + "line": 252, + "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", + "source": "Connected to openclaw-gateway.tailnet.ts.net.", + "surface": "apple", + "id": "native.apple.737eb52b08f2640c" + }, { "kind": "ui-call", "line": 262, @@ -8769,6 +9209,14 @@ "surface": "apple", "id": "native.apple.2b5177b11661390e" }, + { + "kind": "ui-named-argument", + "line": 265, + "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", + "source": "Refreshing connection, discovery, and device trust state.", + "surface": "apple", + "id": "native.apple.1b2d7d34531acc5d" + }, { "kind": "ui-call", "line": 271, @@ -8785,6 +9233,14 @@ "surface": "apple", "id": "native.apple.54df46d3690853c8" }, + { + "kind": "ui-named-argument", + "line": 274, + "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", + "source": "Scan a setup QR code, paste a setup code, or choose a discovered gateway.", + "surface": "apple", + "id": "native.apple.856d2ed1c64b9da1" + }, { "kind": "ui-call", "line": 280, @@ -8793,6 +9249,14 @@ "surface": "apple", "id": "native.apple.2f70e6702c85bedf" }, + { + "kind": "ui-named-argument", + "line": 283, + "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", + "source": "Retry", + "surface": "apple", + "id": "native.apple.1c77c892ee366056" + }, { "kind": "ui-named-argument", "line": 287, @@ -8801,6 +9265,14 @@ "surface": "apple", "id": "native.apple.eadc497b8d3f3b5e" }, + { + "kind": "ui-named-argument", + "line": 288, + "path": "apps/ios/Sources/Design/SettingsProTabSupport.swift", + "source": "Tailscale is off on this device. Turn it on, then try again.", + "surface": "apple", + "id": "native.apple.50af8defa8d7594e" + }, { "kind": "ui-call", "line": 337, @@ -8875,119 +9347,23 @@ }, { "kind": "ui-call", - "line": 64, + "line": 63, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Not Now", "surface": "apple", "id": "native.apple.c6e171128190d131" }, { - "kind": "ui-call", - "line": 118, + "kind": "ui-named-argument", + "line": 110, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Talk", "surface": "apple", - "id": "native.apple.29b77009bc088199" - }, - { - "kind": "ui-named-argument", - "line": 194, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Conversation", - "surface": "apple", - "id": "native.apple.09a3f13ceade420a" - }, - { - "kind": "ui-named-argument", - "line": 198, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Agent", - "surface": "apple", - "id": "native.apple.d0657111b0661e0c" - }, - { - "kind": "ui-named-argument", - "line": 202, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Session", - "surface": "apple", - "id": "native.apple.a08c31cd053b7fc1" - }, - { - "kind": "ui-named-argument", - "line": 205, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Runtime", - "surface": "apple", - "id": "native.apple.330416692795aab8" - }, - { - "kind": "ui-named-argument", - "line": 215, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Voice mode", - "surface": "apple", - "id": "native.apple.922de93e78d7fd5a" - }, - { - "kind": "ui-named-argument", - "line": 224, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Configured", - "surface": "apple", - "id": "native.apple.814578be6a0bd52d" - }, - { - "kind": "ui-named-argument", - "line": 229, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Active now", - "surface": "apple", - "id": "native.apple.823fb69935f72ad6" - }, - { - "kind": "ui-named-argument", - "line": 232, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Transport", - "surface": "apple", - "id": "native.apple.853d3a900164c082" - }, - { - "kind": "ui-named-argument", - "line": 235, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Last issue", - "surface": "apple", - "id": "native.apple.4e7988395204e892" - }, - { - "kind": "ui-named-argument", - "line": 238, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Permission", - "surface": "apple", - "id": "native.apple.e0b5ce7383652059" - }, - { - "kind": "ui-named-argument", - "line": 240, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Speech language", - "surface": "apple", - "id": "native.apple.7cf91b2f6d2f1cf1" - }, - { - "kind": "ui-named-argument", - "line": 249, - "path": "apps/ios/Sources/Design/TalkProTab.swift", - "source": "Controls", - "surface": "apple", - "id": "native.apple.d94c6511c4921adb" + "id": "native.apple.96d3e8f61c156202" }, { "kind": "ui-call", - "line": 253, + "line": 162, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Speakerphone", "surface": "apple", @@ -8995,7 +9371,7 @@ }, { "kind": "ui-call", - "line": 255, + "line": 164, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Background listening", "surface": "apple", @@ -9003,7 +9379,7 @@ }, { "kind": "ui-call", - "line": 259, + "line": 168, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Voice & Talk settings", "surface": "apple", @@ -9011,7 +9387,7 @@ }, { "kind": "conditional-branch", - "line": 505, + "line": 330, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Gateway permission required", "surface": "apple", @@ -9019,7 +9395,7 @@ }, { "kind": "conditional-branch", - "line": 507, + "line": 332, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Requesting approval", "surface": "apple", @@ -9027,7 +9403,7 @@ }, { "kind": "conditional-branch", - "line": 509, + "line": 334, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Approval requested", "surface": "apple", @@ -9035,7 +9411,7 @@ }, { "kind": "conditional-branch", - "line": 511, + "line": 336, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Voice API key missing", "surface": "apple", @@ -9043,7 +9419,7 @@ }, { "kind": "conditional-branch", - "line": 513, + "line": 338, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Voice config failed", "surface": "apple", @@ -9051,7 +9427,7 @@ }, { "kind": "conditional-branch", - "line": 531, + "line": 356, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Needs approval", "surface": "apple", @@ -9059,7 +9435,7 @@ }, { "kind": "conditional-branch", - "line": 533, + "line": 358, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Pending", "surface": "apple", @@ -9067,7 +9443,7 @@ }, { "kind": "conditional-branch", - "line": 535, + "line": 360, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "API key", "surface": "apple", @@ -9075,7 +9451,7 @@ }, { "kind": "conditional-branch", - "line": 537, + "line": 362, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Config", "surface": "apple", @@ -9083,7 +9459,7 @@ }, { "kind": "conditional-branch", - "line": 603, + "line": 428, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Start Talk", "surface": "apple", @@ -9091,7 +9467,7 @@ }, { "kind": "conditional-branch", - "line": 604, + "line": 429, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Stop Talk", "surface": "apple", @@ -9099,7 +9475,7 @@ }, { "kind": "conditional-branch", - "line": 605, + "line": 430, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Enable Talk", "surface": "apple", @@ -9107,7 +9483,7 @@ }, { "kind": "conditional-branch", - "line": 606, + "line": 431, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Open Gateway Settings", "surface": "apple", @@ -9115,7 +9491,7 @@ }, { "kind": "conditional-branch", - "line": 606, + "line": 431, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Open Voice Settings", "surface": "apple", @@ -9123,7 +9499,7 @@ }, { "kind": "conditional-branch", - "line": 607, + "line": 432, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Demo Mode Only", "surface": "apple", @@ -9131,23 +9507,31 @@ }, { "kind": "conditional-branch", - "line": 607, + "line": 432, "path": "apps/ios/Sources/Design/TalkProTab.swift", "source": "Waiting for Approval", "surface": "apple", "id": "native.apple.12c9d8231777366c" }, { - "kind": "ui-call", - "line": 50, + "kind": "ui-named-argument", + "line": 17, + "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", + "source": "Open Settings", + "surface": "apple", + "id": "native.apple.dc57861a3ac5a08e" + }, + { + "kind": "ui-named-argument", + "line": 19, "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", "source": "Details", "surface": "apple", - "id": "native.apple.533ef094239ad275" + "id": "native.apple.8218d64ec7ae90cc" }, { "kind": "ui-call", - "line": 104, + "line": 58, "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", "source": "Technical details", "surface": "apple", @@ -9155,7 +9539,7 @@ }, { "kind": "ui-call", - "line": 109, + "line": 63, "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", "source": "Copy diagnostics", "surface": "apple", @@ -9163,7 +9547,7 @@ }, { "kind": "ui-modifier", - "line": 123, + "line": 77, "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", "source": "Talk fallback", "surface": "apple", @@ -9171,15 +9555,7 @@ }, { "kind": "ui-call", - "line": 128, - "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", - "source": "Open Settings", - "surface": "apple", - "id": "native.apple.1b802aea97c74722" - }, - { - "kind": "ui-call", - "line": 135, + "line": 89, "path": "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift", "source": "Done", "surface": "apple", @@ -9370,24 +9746,16 @@ "id": "native.apple.f21a8e304b607dc8" }, { - "kind": "ui-call", - "line": 39, - "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", - "source": "Request ID: \\(requestId)", - "surface": "apple", - "id": "native.apple.9958308c545765cc" - }, - { - "kind": "ui-call", - "line": 54, + "kind": "ui-named-argument", + "line": 21, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Details", "surface": "apple", - "id": "native.apple.b0933a18a539aef6" + "id": "native.apple.a7866a6499f4462a" }, { "kind": "conditional-branch", - "line": 111, + "line": 63, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Fix on gateway", "surface": "apple", @@ -9395,7 +9763,7 @@ }, { "kind": "conditional-branch", - "line": 113, + "line": 65, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Fix on this device", "surface": "apple", @@ -9403,7 +9771,7 @@ }, { "kind": "conditional-branch", - "line": 115, + "line": 67, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Check both", "surface": "apple", @@ -9411,7 +9779,7 @@ }, { "kind": "conditional-branch", - "line": 117, + "line": 69, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Check network", "surface": "apple", @@ -9419,7 +9787,7 @@ }, { "kind": "conditional-branch", - "line": 119, + "line": 71, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Needs attention", "surface": "apple", @@ -9427,7 +9795,7 @@ }, { "kind": "ui-call", - "line": 153, + "line": 105, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Request", "surface": "apple", @@ -9435,7 +9803,7 @@ }, { "kind": "ui-call", - "line": 157, + "line": 109, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Copy request ID", "surface": "apple", @@ -9443,7 +9811,7 @@ }, { "kind": "ui-call", - "line": 165, + "line": 117, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Gateway command", "surface": "apple", @@ -9451,7 +9819,7 @@ }, { "kind": "ui-call", - "line": 169, + "line": 121, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Copy command", "surface": "apple", @@ -9459,7 +9827,7 @@ }, { "kind": "ui-call", - "line": 177, + "line": 129, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Help", "surface": "apple", @@ -9467,7 +9835,7 @@ }, { "kind": "ui-call", - "line": 179, + "line": 131, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Open docs", "surface": "apple", @@ -9475,7 +9843,7 @@ }, { "kind": "ui-call", - "line": 189, + "line": 141, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Technical details", "surface": "apple", @@ -9483,7 +9851,7 @@ }, { "kind": "ui-modifier", - "line": 205, + "line": 157, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Connection problem", "surface": "apple", @@ -9491,7 +9859,7 @@ }, { "kind": "ui-call", - "line": 217, + "line": 169, "path": "apps/ios/Sources/Gateway/GatewayProblemView.swift", "source": "Done", "surface": "apple", @@ -10377,14 +10745,6 @@ "surface": "apple", "id": "native.apple.6c3fe084ab7b318d" }, - { - "kind": "ui-call", - "line": 155, - "path": "apps/ios/Sources/RootTabs.swift", - "source": "Chat", - "surface": "apple", - "id": "native.apple.fa30f77325b46efb" - }, { "kind": "ui-call", "line": 162, @@ -10419,7 +10779,7 @@ }, { "kind": "ui-call", - "line": 286, + "line": 287, "path": "apps/ios/Sources/RootTabs.swift", "source": "OpenClaw", "surface": "apple", @@ -10427,7 +10787,7 @@ }, { "kind": "ui-modifier", - "line": 315, + "line": 316, "path": "apps/ios/Sources/RootTabs.swift", "source": "OpenClaw \\(self.sidebarGatewayStatusTitle)", "surface": "apple", @@ -10435,7 +10795,7 @@ }, { "kind": "conditional-branch", - "line": 320, + "line": 321, "path": "apps/ios/Sources/RootTabs.swift", "source": "Online", "surface": "apple", @@ -10443,7 +10803,7 @@ }, { "kind": "conditional-branch", - "line": 322, + "line": 323, "path": "apps/ios/Sources/RootTabs.swift", "source": "Connecting", "surface": "apple", @@ -10451,7 +10811,7 @@ }, { "kind": "conditional-branch", - "line": 324, + "line": 325, "path": "apps/ios/Sources/RootTabs.swift", "source": "Needs attention", "surface": "apple", @@ -10459,12 +10819,68 @@ }, { "kind": "conditional-branch", - "line": 326, + "line": 327, "path": "apps/ios/Sources/RootTabs.swift", "source": "Offline", "surface": "apple", "id": "native.apple.7da053dd774cb201" }, + { + "kind": "ui-named-argument", + "line": 405, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Chat", + "surface": "apple", + "id": "native.apple.a4256b2ab523ee60" + }, + { + "kind": "ui-named-argument", + "line": 418, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Overview", + "surface": "apple", + "id": "native.apple.64d7dc062ef4cc81" + }, + { + "kind": "ui-named-argument", + "line": 442, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Agents", + "surface": "apple", + "id": "native.apple.11f98fe0650d52f5" + }, + { + "kind": "ui-named-argument", + "line": 449, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Instances", + "surface": "apple", + "id": "native.apple.47426e84372bc0a5" + }, + { + "kind": "ui-named-argument", + "line": 460, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Dreaming", + "surface": "apple", + "id": "native.apple.ad3c65353227b63e" + }, + { + "kind": "ui-named-argument", + "line": 467, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Usage", + "surface": "apple", + "id": "native.apple.a3ba593df67a8673" + }, + { + "kind": "ui-named-argument", + "line": 474, + "path": "apps/ios/Sources/RootTabs.swift", + "source": "Cron Jobs", + "surface": "apple", + "id": "native.apple.db0d9af6d0855d18" + }, { "kind": "ui-modifier", "line": 579, @@ -10641,126 +11057,6 @@ "surface": "apple", "id": "native.apple.e69088f1f2dcbd99" }, - { - "kind": "conditional-branch", - "line": 74, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Agent chat and recent work.", - "surface": "apple", - "id": "native.apple.18e9dff84e596eb1" - }, - { - "kind": "conditional-branch", - "line": 75, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Realtime voice and fallback controls.", - "surface": "apple", - "id": "native.apple.fa0241ea8d6346c1" - }, - { - "kind": "conditional-branch", - "line": 76, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Status, entry points, health.", - "surface": "apple", - "id": "native.apple.82f76513564adb04" - }, - { - "kind": "conditional-branch", - "line": 77, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Gateway, session, and device activity.", - "surface": "apple", - "id": "native.apple.96ce004d0e07fbed" - }, - { - "kind": "conditional-branch", - "line": 78, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Agent roster and readiness.", - "surface": "apple", - "id": "native.apple.78452391251218ef" - }, - { - "kind": "conditional-branch", - "line": 79, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Agent work queue and session handoff.", - "surface": "apple", - "id": "native.apple.dc16558f3a6a72e5" - }, - { - "kind": "conditional-branch", - "line": 80, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Review and apply proposed skills.", - "surface": "apple", - "id": "native.apple.59fd1c6bd08199d4" - }, - { - "kind": "conditional-branch", - "line": 81, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Latest presence from OpenClaw nodes.", - "surface": "apple", - "id": "native.apple.cb673576875bc369" - }, - { - "kind": "conditional-branch", - "line": 82, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Active sessions and defaults.", - "surface": "apple", - "id": "native.apple.d660d728325c5257" - }, - { - "kind": "conditional-branch", - "line": 83, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Memory signals and background synthesis.", - "surface": "apple", - "id": "native.apple.9329a83abeb6f653" - }, - { - "kind": "conditional-branch", - "line": 84, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "API usage and costs.", - "surface": "apple", - "id": "native.apple.e1dfaa7239af4048" - }, - { - "kind": "conditional-branch", - "line": 85, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Wakeups and recurring runs.", - "surface": "apple", - "id": "native.apple.6a5d8adf3b18049e" - }, - { - "kind": "conditional-branch", - "line": 86, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Reference docs and setup guides.", - "surface": "apple", - "id": "native.apple.555bb8c590ec64ae" - }, - { - "kind": "conditional-branch", - "line": 87, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Connection, permissions, channels, and app options.", - "surface": "apple", - "id": "native.apple.bffa9eb6bc3156af" - }, - { - "kind": "conditional-branch", - "line": 88, - "path": "apps/ios/Sources/RootTabsNavigation.swift", - "source": "Pairing, diagnostics, permissions, and device controls.", - "surface": "apple", - "id": "native.apple.83d649015174a243" - }, { "kind": "ui-call", "line": 14, @@ -10777,6 +11073,14 @@ "surface": "apple", "id": "native.apple.4dac301c7e0c921c" }, + { + "kind": "ui-named-argument", + "line": 19, + "path": "apps/ios/Sources/Settings/PrivacyAccessSectionView.swift", + "source": "Search and add contacts from the assistant.", + "surface": "apple", + "id": "native.apple.01f362d70f6ddb4e" + }, { "kind": "ui-named-argument", "line": 24, @@ -10785,6 +11089,14 @@ "surface": "apple", "id": "native.apple.df0159ff34744870" }, + { + "kind": "ui-named-argument", + "line": 27, + "path": "apps/ios/Sources/Settings/PrivacyAccessSectionView.swift", + "source": "Add events with least privilege.", + "surface": "apple", + "id": "native.apple.c6db4240083c0435" + }, { "kind": "ui-named-argument", "line": 32, @@ -10793,6 +11105,14 @@ "surface": "apple", "id": "native.apple.dc62e22f0b2726c5" }, + { + "kind": "ui-named-argument", + "line": 35, + "path": "apps/ios/Sources/Settings/PrivacyAccessSectionView.swift", + "source": "List and read calendar events.", + "surface": "apple", + "id": "native.apple.650091bc632b3120" + }, { "kind": "ui-named-argument", "line": 40, @@ -10801,6 +11121,14 @@ "surface": "apple", "id": "native.apple.7a81d1f17d9d90c0" }, + { + "kind": "ui-named-argument", + "line": 43, + "path": "apps/ios/Sources/Settings/PrivacyAccessSectionView.swift", + "source": "List, add, and complete reminders.", + "surface": "apple", + "id": "native.apple.d6fd5e9f85b0e298" + }, { "kind": "conditional-branch", "line": 236, @@ -10883,7 +11211,7 @@ }, { "kind": "ui-modifier", - "line": 26, + "line": 21, "path": "apps/ios/Sources/Status/VoiceWakeToast.swift", "source": "Voice Wake triggered", "surface": "apple", @@ -15785,6 +16113,14 @@ "surface": "apple", "id": "native.apple.568b8fcf5608d3ea" }, + { + "kind": "ui-named-argument", + "line": 837, + "path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift", + "source": "Open Settings → Channels", + "surface": "apple", + "id": "native.apple.c37c668c737e8b71" + }, { "kind": "ui-named-argument", "line": 842, @@ -15833,6 +16169,14 @@ "surface": "apple", "id": "native.apple.fea44c40dc512455" }, + { + "kind": "ui-named-argument", + "line": 854, + "path": "apps/macos/Sources/OpenClaw/OnboardingView+Pages.swift", + "source": "Open Settings → Skills", + "surface": "apple", + "id": "native.apple.420781e5adca4122" + }, { "kind": "ui-call", "line": 859, @@ -18307,7 +18651,7 @@ }, { "kind": "conditional-branch", - "line": 378, + "line": 379, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Talk", "surface": "apple", @@ -18315,7 +18659,7 @@ }, { "kind": "conditional-branch", - "line": 420, + "line": 421, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Start realtime chat", "surface": "apple", @@ -18323,7 +18667,7 @@ }, { "kind": "conditional-branch", - "line": 420, + "line": 421, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop realtime chat", "surface": "apple", @@ -18331,7 +18675,7 @@ }, { "kind": "conditional-branch", - "line": 459, + "line": 460, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Start", "surface": "apple", @@ -18339,7 +18683,7 @@ }, { "kind": "conditional-branch", - "line": 459, + "line": 460, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop", "surface": "apple", @@ -18347,7 +18691,7 @@ }, { "kind": "ui-modifier", - "line": 557, + "line": 555, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Stop response", "surface": "apple", @@ -18355,7 +18699,7 @@ }, { "kind": "ui-modifier", - "line": 581, + "line": 579, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Send message", "surface": "apple", @@ -18363,7 +18707,7 @@ }, { "kind": "ui-modifier", - "line": 595, + "line": 593, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Refresh", "surface": "apple", @@ -18371,7 +18715,7 @@ }, { "kind": "conditional-branch", - "line": 677, + "line": 679, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Connecting...", "surface": "apple", @@ -18379,7 +18723,7 @@ }, { "kind": "conditional-branch", - "line": 677, + "line": 679, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift", "source": "Gateway connected", "surface": "apple", @@ -18441,6 +18785,14 @@ "surface": "apple", "id": "native.apple.43be8a2336e42faf" }, + { + "kind": "ui-named-argument", + "line": 365, + "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", + "source": "Refresh", + "surface": "apple", + "id": "native.apple.9ab586b85834705c" + }, { "kind": "ui-call", "line": 675, @@ -18449,14 +18801,6 @@ "surface": "apple", "id": "native.apple.28c2691c03030434" }, - { - "kind": "ui-modifier", - "line": 766, - "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift", - "source": "Refresh", - "surface": "apple", - "id": "native.apple.57f7e5c40ed0acc6" - }, { "kind": "ui-modifier", "line": 773, diff --git a/apps/ios/DESIGN.md b/apps/ios/DESIGN.md new file mode 100644 index 00000000000..98c93b86dc1 --- /dev/null +++ b/apps/ios/DESIGN.md @@ -0,0 +1,55 @@ +# iOS design system + +OpenClaw follows the native iOS 26 design language while keeping an iOS 18 deployment target. Use SwiftUI system structure first, Liquid Glass for interactive chrome, and quiet opaque surfaces for content. + +## Principles + +- Prefer `NavigationStack`, `TabView`, `List`, `Form`, toolbars, sheets, and system controls. They adopt the current platform appearance automatically. +- Reserve Liquid Glass for navigation and interactive controls. Do not apply glass to every card, row, or status surface. +- Keep content hierarchy clear with typography, spacing, and grouping before adding backgrounds. +- Use semantic colors. Red means destructive or stopped; orange means attention; green means healthy. Neutral actions use the app accent. +- Preserve Dynamic Type, VoiceOver labels, Reduce Motion, increased contrast, and 44-point touch targets. +- Use continuous corners and concentric geometry. Nested controls should visually follow their container shape. + +Apple references: [Adopting Liquid Glass](https://developer.apple.com/documentation/technologyoverviews/adopting-liquid-glass), [Applying Liquid Glass to custom views](https://developer.apple.com/documentation/swiftui/applying-liquid-glass-to-custom-views), and [Build a SwiftUI app with the new design](https://developer.apple.com/videos/play/wwdc2025/323/). + +## Tokens + +`OpenClawProMetric` in `Sources/Design/OpenClawProComponents.swift` is the source of truth for shared geometry: + +- `pagePadding`: standard page gutter +- `cardRadius`: content group radius +- `controlRadius`: inset control radius +- `compactControlSize`: compact circular control size +- `bottomScrollInset`: clearance above persistent navigation + +Feature-local layout enums may define row heights and grid dimensions, but should reference the shared radius instead of introducing a new card shape. + +## Components + +- `OpenClawProBackground`: grouped page background +- `ProCard`: quiet content grouping; never Liquid Glass +- `ProIconBadge`, `ProValuePill`: compact semantic indicators +- `OpenClawNoticeBanner`: shared connection and runtime notices +- `OpenClawAdaptiveHeaderRow`: responsive destination heading +- `OpenClawGlassControlGroup`: performance and morphing boundary for nearby glass controls +- `openClawGlassButton(prominent:tint:)`: iOS 26 glass button with an iOS 18 bordered fallback +- `openClawTabBarBehavior()`: iOS 26 tab-bar minimization with an earlier-system no-op + +## Liquid Glass rules + +Use `openClawGlassButton` for primary actions, compact header controls, and navigation-adjacent controls. Use the prominent style for one primary action per region. Wrap nearby controls in `OpenClawGlassControlGroup`. + +Do not place Liquid Glass behind reading content, forms, metrics, or every card in a scroll view. Excess glass weakens hierarchy, increases rendering cost, and competes with the system tab bar and navigation chrome. + +Keep new iOS APIs behind `#available(iOS 26.0, *)`. The fallback must preserve the same label, action, tint meaning, accessibility, and approximate hit target. + +## Review checklist + +- Uses a native container or control where one exists. +- Uses shared spacing and corner tokens. +- Has one obvious primary action. +- Keeps semantic color independent from decoration. +- Works in light and dark mode, Dynamic Type, and compact phone layouts. +- Verifies iOS 26 appearance in the simulator and preserves the iOS 18 fallback path. +- Adds matched before/after evidence for a visual change. diff --git a/apps/ios/README.md b/apps/ios/README.md index e746936cfdb..24f1f739d35 100644 --- a/apps/ios/README.md +++ b/apps/ios/README.md @@ -18,7 +18,7 @@ This iOS app is super-alpha and internal-use only. The first public App Store re ## Exact Xcode Manual Deploy Flow 1. Prereqs: - - Xcode 16+ + - Xcode 26.x - `pnpm` - `xcodegen` - Apple Development signing set up in Xcode @@ -51,7 +51,7 @@ pnpm ios:open Prereqs: -- Xcode 16+ +- Xcode 26.x - `pnpm` - `xcodegen` - `fastlane` diff --git a/apps/ios/Sources/Design/AgentProTab+Destinations.swift b/apps/ios/Sources/Design/AgentProTab+Destinations.swift index 1b8fd48a2f4..c034dfa87dc 100644 --- a/apps/ios/Sources/Design/AgentProTab+Destinations.swift +++ b/apps/ios/Sources/Design/AgentProTab+Destinations.swift @@ -37,8 +37,9 @@ extension AgentProTab { } .safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset) } - .navigationTitle("Agents") - .navigationBarTitleDisplayMode(.inline) + // The roster owns its large adaptive header; a second navigation title + // wastes vertical space and makes the phone tab look double-stacked. + .toolbar(.hidden, for: .navigationBar) } var skillsDestination: some View { diff --git a/apps/ios/Sources/Design/AgentProTab+GatewayData.swift b/apps/ios/Sources/Design/AgentProTab+GatewayData.swift index f6dee7e20f3..294b92a87df 100644 --- a/apps/ios/Sources/Design/AgentProTab+GatewayData.swift +++ b/apps/ios/Sources/Design/AgentProTab+GatewayData.swift @@ -29,32 +29,20 @@ extension AgentProTab { func agentDetail(for agent: AgentSummary) -> String { let parts = [ - self.normalized(agent.workspace), self.modelLabel(for: agent), - agent.id == self.appModel.gatewayDefaultAgentId ? "default" : nil, + agent.id == self.appModel.gatewayDefaultAgentId ? "Default" : nil, ].compactMap(\.self) return parts.isEmpty ? agent.id : parts.joined(separator: " • ") } - func agentSessionSummary(_ agent: AgentSummary) -> String { - guard self.gatewayConnected else { return "0" } - if agent.id == self.activeAgentID { - return self.appModel.isOperatorGatewayConnected ? "1 running" : "0" - } - return "0" - } - - func agentRuntimeSummary(_ agent: AgentSummary) -> String { - if let runtime = agent.agentruntime, - let id = runtime["id"]?.value as? String, - let normalized = self.normalized(id) - { - return normalized - } - if let model = self.modelLabel(for: agent) { - return Self.shortModelLabel(model) - } - return "default" + func agentAccessibilityLabel( + _ agent: AgentSummary, + isActive: Bool, + state: AgentRosterState) -> String + { + let status = state == .online ? "Online" : "Ready" + let selection = isActive ? "Selected" : "Not selected" + return "\(self.agentName(for: agent)), \(self.agentDetail(for: agent)), \(status), \(selection)" } func agentRosterState(for agent: AgentSummary) -> AgentRosterState { @@ -75,15 +63,6 @@ extension AgentProTab { return nil } - static func shortModelLabel(_ model: String) -> String { - let trimmed = model.trimmingCharacters(in: .whitespacesAndNewlines) - guard !trimmed.isEmpty else { return "default" } - let leaf = trimmed.split(separator: "/").last.map(String.init) ?? trimmed - return leaf - .replacingOccurrences(of: "claude-", with: "") - .replacingOccurrences(of: "gpt-", with: "") - } - func presenceLabel(_ entry: PresenceEntry) -> String? { self.normalized(entry.host) ?? self.normalized(entry.devicefamily) diff --git a/apps/ios/Sources/Design/AgentProTab+Overview.swift b/apps/ios/Sources/Design/AgentProTab+Overview.swift index 07d3469e5d6..16574d34858 100644 --- a/apps/ios/Sources/Design/AgentProTab+Overview.swift +++ b/apps/ios/Sources/Design/AgentProTab+Overview.swift @@ -16,22 +16,18 @@ extension AgentProTab { OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction) } } accessory: { - HStack(spacing: 10) { - self.gatewayPillButton - self.headerIconButton( - systemName: "magnifyingglass", - label: "Search agents", - action: { - withAnimation(.snappy(duration: 0.18)) { - self.agentSearchPresented.toggle() - } - }) - self.headerIconButton( - systemName: "arrow.clockwise", - label: self.overviewLoading ? "Refreshing agents" : "Refresh agents", - action: { - self.overviewRefreshNonce += 1 - }) + OpenClawGlassControlGroup { + HStack(spacing: 10) { + self.gatewayPillButton + self.headerIconButton( + systemName: "magnifyingglass", + label: "Search agents", + action: { + withAnimation(.snappy(duration: 0.18)) { + self.agentSearchPresented.toggle() + } + }) + } } .padding(.top, 2) } @@ -41,15 +37,8 @@ extension AgentProTab { .textInputAutocapitalization(.never) .autocorrectionDisabled() .font(.subheadline) - .padding(.horizontal, 12) + .textFieldStyle(.roundedBorder) .frame(height: 38) - .background { - Capsule() - .fill(self.searchFieldFill) - .overlay { - Capsule().strokeBorder(self.searchFieldStroke, lineWidth: 1) - } - } .transition(.move(edge: .top).combined(with: .opacity)) } } @@ -63,7 +52,8 @@ extension AgentProTab { Button(action: openSettings) { OpenClawGatewayCompactPill() } - .buttonStyle(.plain) + .buttonBorderShape(.capsule) + .openClawGlassButton() .accessibilityHint("Opens Settings / Gateway") } else { OpenClawGatewayCompactPill() @@ -71,46 +61,32 @@ extension AgentProTab { } var agentFilters: some View { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { + HStack(spacing: 10) { + Picker("Agent status", selection: self.$agentRosterFilter) { ForEach(AgentRosterFilter.allCases) { filter in - Button { - withAnimation(.snappy(duration: 0.18)) { - self.agentRosterFilter = filter - } - } label: { - Text(filter.title) - .font(.caption.weight(.semibold)) - .foregroundStyle(self.agentRosterFilter == filter ? .primary : .secondary) - .padding(.horizontal, 15) - .frame(height: AgentLayout.filterHeight) - .background { - Capsule() - .fill(self.agentRosterFilter == filter - ? Color.primary.opacity(0.13) - : Color.primary.opacity(0.055)) - } - .overlay { - Capsule() - .strokeBorder(Color.primary.opacity(self.agentRosterFilter == filter ? 0.22 : 0.06)) - } - } - .buttonStyle(.plain) - } - - if self.agentFiltersActive { - self.headerIconButton( - systemName: "xmark", - label: "Clear filters", - action: { - self.agentRosterFilter = .all - self.agentSearchText = "" - }) - .frame(width: AgentLayout.filterHeight, height: AgentLayout.filterHeight) + Text(filter.title).tag(filter) } } - .padding(.horizontal, OpenClawProMetric.pagePadding) + .pickerStyle(.segmented) + + if self.agentFiltersActive { + Button { + withAnimation(.snappy(duration: 0.18)) { + self.agentRosterFilter = .all + self.agentSearchText = "" + } + } label: { + Image(systemName: "xmark.circle.fill") + .font(.title3) + .foregroundStyle(.secondary) + .frame(width: 44, height: 44) + .contentShape(Circle()) + } + .buttonStyle(.plain) + .accessibilityLabel("Clear filters") + } } + .padding(.horizontal, OpenClawProMetric.pagePadding) } var agentFiltersActive: Bool { @@ -246,68 +222,44 @@ extension AgentProTab { func agentRow(_ agent: AgentSummary) -> some View { let isActive = agent.id == self.activeAgentID let state = self.agentRosterState(for: agent) - return HStack(alignment: .top, spacing: 12) { - self.agentAvatar(agent, state: state) + return Button { + guard !isActive else { return } + self.appModel.setSelectedAgentId(agent.id) + } label: { + HStack(alignment: .center, spacing: 12) { + self.agentAvatar(agent, state: state) - VStack(alignment: .leading, spacing: 8) { - VStack(alignment: .leading, spacing: 2) { - HStack(spacing: 6) { - Text(self.agentName(for: agent)) - .font(.subheadline.weight(.semibold)) - .lineLimit(1) - - HStack(spacing: 4) { - Circle() - .fill(state.color) - .frame(width: 6, height: 6) - Text(state.title) - .font(.caption2.weight(.semibold)) - } - .foregroundStyle(state.color) + VStack(alignment: .leading, spacing: 3) { + Text(self.agentName(for: agent)) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.primary) .lineLimit(1) - } Text(self.agentDetail(for: agent)) - .font(.caption) + .font(.footnote) .foregroundStyle(.secondary) .lineLimit(1) } + .layoutPriority(1) - HStack(spacing: 0) { - self.agentMetric(label: "Sessions", value: self.agentSessionSummary(agent)) - Divider() - .frame(height: 24) - .padding(.horizontal, 12) - self.agentMetric(label: "Runtime", value: self.agentRuntimeSummary(agent)) + Spacer(minLength: 8) + + if isActive { + Image(systemName: "checkmark") + .font(.subheadline.weight(.semibold)) + .foregroundStyle(OpenClawBrand.accent) + .frame(width: 24, height: 44) + .accessibilityHidden(true) } } - .layoutPriority(1) - - Button { - self.appModel.setSelectedAgentId(agent.id) - } label: { - Image(systemName: isActive ? "checkmark" : "arrow.right") - .font(.caption.weight(.bold)) - } - .buttonStyle(.plain) - .foregroundStyle(isActive ? OpenClawBrand.accent : .primary) - .frame(width: AgentLayout.actionButtonSize, height: AgentLayout.actionButtonSize) - .background { - Circle() - .fill(self.iconButtonFill) - .overlay { - Circle().strokeBorder(self.iconButtonStroke, lineWidth: 1) - } - } - .accessibilityLabel(isActive ? "Default agent" : "Set default agent") - } - .padding(.vertical, 14) - .padding(.horizontal, 13) - .frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading) - .contentShape(Rectangle()) - .onTapGesture { - self.appModel.setSelectedAgentId(agent.id) + .padding(.vertical, 10) + .padding(.horizontal, 13) + .frame(maxWidth: .infinity, minHeight: AgentLayout.rowMinHeight, alignment: .leading) + .contentShape(Rectangle()) } + .buttonStyle(.plain) + .accessibilityLabel(self.agentAccessibilityLabel(agent, isActive: isActive, state: state)) + .accessibilityHint(isActive ? "Selected agent" : "Selects this agent") } func headerIconButton( @@ -319,15 +271,9 @@ extension AgentProTab { Image(systemName: systemName) .font(.subheadline.weight(.semibold)) .frame(width: AgentLayout.filterHeight, height: AgentLayout.filterHeight) - .background { - Circle() - .fill(self.iconButtonFill) - .overlay { - Circle().strokeBorder(self.iconButtonStroke, lineWidth: 1) - } - } } - .buttonStyle(.plain) + .buttonBorderShape(.circle) + .openClawGlassButton() .accessibilityLabel(label) } @@ -338,40 +284,19 @@ extension AgentProTab { .foregroundStyle(.white) .minimumScaleFactor(0.62) .lineLimit(1) - .frame(width: 48, height: 48) + .frame(width: 42, height: 42) .background( Circle() - .fill( - LinearGradient( - colors: [ - self.agentTint(for: agent, state: state), - Color.primary.opacity(0.38), - ], - startPoint: .topLeading, - endPoint: .bottomTrailing))) + .fill(self.agentTint(for: agent, state: state).gradient)) .overlay(Circle().strokeBorder(Color.white.opacity(0.18), lineWidth: 1)) Circle() .fill(state.color) - .frame(width: 10, height: 10) - .overlay(Circle().strokeBorder(Color.primary.opacity(0.15), lineWidth: 1)) + .frame(width: 9, height: 9) + .overlay(Circle().strokeBorder(Color(uiColor: .systemBackground), lineWidth: 2)) } } - func agentMetric(label: String, value: String) -> some View { - VStack(alignment: .leading, spacing: 2) { - Text(label) - .font(.caption2) - .foregroundStyle(.secondary) - Text(value) - .font(.caption.weight(.semibold)) - .foregroundStyle(.primary) - .lineLimit(1) - .minimumScaleFactor(0.74) - } - .frame(minWidth: 60, alignment: .leading) - } - func agentMenuRow( icon: String, title: String, @@ -562,22 +487,6 @@ extension AgentProTab { self.appModel.isOperatorGatewayConnected } - private var searchFieldFill: Color { - self.colorScheme == .dark ? Color.white.opacity(0.045) : Color.white.opacity(0.78) - } - - private var searchFieldStroke: Color { - self.colorScheme == .dark ? Color.white.opacity(0.11) : Color.black.opacity(0.07) - } - - private var iconButtonFill: Color { - self.colorScheme == .dark ? Color.white.opacity(0.065) : Color.white.opacity(0.78) - } - - private var iconButtonStroke: Color { - self.colorScheme == .dark ? Color.white.opacity(0.14) : Color.black.opacity(0.07) - } - var emptyAgentsTitle: String { if !self.gatewayConnected { return "Agents unavailable" } if !self.agentSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return "No matches" } @@ -600,7 +509,6 @@ extension AgentProTab { self.appModel.isOperatorGatewayConnected ? "operator" : "no-operator", self.activeAgentID, self.scenePhase == .active ? "active" : "inactive", - "\(self.overviewRefreshNonce)", ].joined(separator: ":") } diff --git a/apps/ios/Sources/Design/AgentProTab.swift b/apps/ios/Sources/Design/AgentProTab.swift index fad9a1f7b72..0ce6ed0c882 100644 --- a/apps/ios/Sources/Design/AgentProTab.swift +++ b/apps/ios/Sources/Design/AgentProTab.swift @@ -3,7 +3,6 @@ import SwiftUI struct AgentProTab: View { @Environment(NodeAppModel.self) var appModel - @Environment(\.colorScheme) var colorScheme @Environment(\.scenePhase) var scenePhase let directRoute: AgentRoute? let headerLeadingAction: OpenClawSidebarHeaderAction? @@ -13,7 +12,6 @@ struct AgentProTab: View { @State var overview: AgentOverviewSnapshot? @State var overviewErrorText: String? @State var overviewLoading: Bool = false - @State var overviewRefreshNonce: Int = 0 @State var agentRosterFilter: AgentRosterFilter = .all @State var agentSearchPresented = false @State var agentSearchText = "" @@ -84,24 +82,16 @@ struct AgentProTab: View { } enum AgentLayout { - static let cardRadius: CGFloat = 12 + static let cardRadius: CGFloat = OpenClawProMetric.cardRadius static let filterHeight: CGFloat = 34 - static let rowMinHeight: CGFloat = 104 + static let rowMinHeight: CGFloat = 72 static let metricTileHeight: CGFloat = 94 - static let actionButtonSize: CGFloat = 34 } enum AgentRosterState: Equatable { case online case ready - var title: String { - switch self { - case .online: "Online" - case .ready: "Ready" - } - } - var color: Color { switch self { case .online: OpenClawBrand.ok @@ -185,7 +175,7 @@ struct AgentProTab: View { private func directDestination(for route: AgentRoute) -> some View { self.destination(for: route) .toolbar( - self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden, + route == .agents || self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible, for: .navigationBar) } } diff --git a/apps/ios/Sources/Design/ChatProTab.swift b/apps/ios/Sources/Design/ChatProTab.swift index cfb20d4c3a4..164638e8210 100644 --- a/apps/ios/Sources/Design/ChatProTab.swift +++ b/apps/ios/Sources/Design/ChatProTab.swift @@ -135,16 +135,9 @@ struct ChatProTab: View { .frame(width: 38, height: 38) .background( Circle() - .fill( - LinearGradient( - colors: [ - OpenClawBrand.accent, - OpenClawBrand.accentHot, - ], - startPoint: .topLeading, - endPoint: .bottomTrailing))) + .fill(OpenClawBrand.accent.gradient)) .overlay(Circle().strokeBorder(.white.opacity(0.18), lineWidth: 1)) - .shadow(color: OpenClawBrand.accent.opacity(0.18), radius: 10, y: 5) + .shadow(color: OpenClawBrand.accent.opacity(0.14), radius: 5, y: 2) } else { ProIconBadge(systemName: "bubble.left", color: OpenClawBrand.accent) } @@ -208,7 +201,8 @@ struct ChatProTab: View { Button(action: openSettings) { self.connectionPill } - .buttonStyle(.plain) + .buttonBorderShape(.capsule) + .openClawGlassButton() .accessibilityHint("Opens Settings / Gateway") } else { self.connectionPill @@ -223,16 +217,8 @@ struct ChatProTab: View { .lineLimit(1) } .foregroundStyle(self.gatewayPillColor) - .padding(.horizontal, 10) + .padding(.horizontal, 4) .frame(height: 30) - .background { - Capsule() - .fill(self.gatewayPillColor.opacity(0.11)) - } - .overlay { - Capsule() - .strokeBorder(self.gatewayPillColor.opacity(0.16), lineWidth: 1) - } } private var gatewayConnected: Bool { @@ -281,8 +267,8 @@ struct ChatProTab: View { ?? Self.defaultHeaderTitle(showsAgentBadge: self.showsAgentBadge, agentDisplayName: self.agentDisplayName) } - private var headerDisplaySubtitle: String { - self.normalized(self.headerSubtitle) ?? "AI Assistant" + private var headerDisplaySubtitle: String? { + self.normalized(self.headerSubtitle) } nonisolated static func defaultHeaderTitle(showsAgentBadge: Bool, agentDisplayName: String) -> String { diff --git a/apps/ios/Sources/Design/CommandCenterSupport.swift b/apps/ios/Sources/Design/CommandCenterSupport.swift index 2125ba36d20..9075fcd28b5 100644 --- a/apps/ios/Sources/Design/CommandCenterSupport.swift +++ b/apps/ios/Sources/Design/CommandCenterSupport.swift @@ -31,22 +31,12 @@ struct CommandPanel: View { } struct CommandControlBackground: View { - @Environment(\.colorScheme) private var colorScheme - var body: some View { - Color(uiColor: self.colorScheme == .dark ? .systemBackground : .systemGroupedBackground) - .overlay(alignment: .top) { - if self.colorScheme == .light { - Color.white.opacity(0.20) - .frame(height: 140) - } - } - .ignoresSafeArea() + OpenClawProBackground() } } struct CommandSessionRow: View { - @Environment(\.colorScheme) private var colorScheme let item: CommandCenterTab.WorkItem var body: some View { @@ -88,16 +78,9 @@ struct CommandSessionRow: View { } } } - .padding(.horizontal, 10) - .padding(.vertical, 8) - .background { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .fill(self.rowFill) - .overlay { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .strokeBorder(self.rowBorder, lineWidth: 1) - } - } + .padding(.horizontal, 4) + .padding(.vertical, 6) + .contentShape(Rectangle()) } private var progressLabel: String { @@ -109,41 +92,16 @@ struct CommandSessionRow: View { } return "\(Int((progress * 100).rounded()))%" } - - private var rowFill: Color { - self.colorScheme == .dark ? Color.white.opacity(0.035) : Color(uiColor: .systemBackground) - } - - private var rowBorder: Color { - Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.24 : 0.22) - } } struct CommandViewMoreRow: View { - @Environment(\.colorScheme) private var colorScheme - var body: some View { - Text("View More") + Label("View More", systemImage: "chevron.right") .font(.subheadline.weight(.bold)) .foregroundStyle(OpenClawBrand.accent) .frame(maxWidth: .infinity) .padding(.vertical, 10) - .background { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .fill(self.rowFill) - .overlay { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .strokeBorder(self.rowBorder, lineWidth: 1) - } - } - } - - private var rowFill: Color { - self.colorScheme == .dark ? Color.white.opacity(0.035) : Color(uiColor: .systemBackground) - } - - private var rowBorder: Color { - Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.24 : 0.22) + .contentShape(Rectangle()) } } @@ -173,15 +131,7 @@ struct CommandEmptyStateRow: View { } Spacer(minLength: 0) } - .padding(.horizontal, 8) - .padding(.vertical, 8) - .background { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .fill(Color(uiColor: .systemBackground)) - .overlay { - RoundedRectangle(cornerRadius: OpenClawProMetric.controlRadius, style: .continuous) - .strokeBorder(Color(uiColor: .separator).opacity(0.22), lineWidth: 1) - } - } + .padding(.horizontal, 4) + .padding(.vertical, 6) } } diff --git a/apps/ios/Sources/Design/CommandCenterTab.swift b/apps/ios/Sources/Design/CommandCenterTab.swift index 4955301205f..fac25d5866e 100644 --- a/apps/ios/Sources/Design/CommandCenterTab.swift +++ b/apps/ios/Sources/Design/CommandCenterTab.swift @@ -119,14 +119,13 @@ struct CommandCenterTab: View { } } accessory: { Button(action: self.openSettings) { - ProCapsule( - title: self.gatewayStateText, - color: self.gatewayStatusColor, - icon: self.gatewayConnected ? "checkmark.circle.fill" : "wifi.slash") + Image(systemName: "gearshape.fill") + .font(.subheadline.weight(.semibold)) + .frame(width: OpenClawProMetric.compactControlSize, height: OpenClawProMetric.compactControlSize) } - .buttonStyle(.plain) - .accessibilityLabel("Gateway \(self.gatewayStateText)") - .accessibilityHint("Opens Settings / Gateway") + .openClawGlassButton() + .accessibilityLabel("Gateway settings") + .accessibilityHint("Opens gateway settings") } .padding(.horizontal, OpenClawProMetric.pagePadding) } @@ -150,17 +149,13 @@ struct CommandCenterTab: View { private var gatewayCard: some View { CommandPanel(isProminent: true, padding: 12) { VStack(alignment: .leading, spacing: 10) { - self.cardHeader( - title: "Gateway", - value: self.gatewayStateText, - color: self.gatewayStatusColor, - icon: self.gatewayConnected ? "checkmark.circle.fill" : "wifi.slash") + self.cardHeader(title: "Gateway") HStack(spacing: 0) { self.gatewayFact( icon: "network", title: "Connection", - value: self.gatewayConnected ? "Online" : "Offline", + value: self.gatewayConnectionText, color: self.gatewayStatusColor) Divider().frame(height: 38) self.gatewayFact( @@ -175,17 +170,7 @@ struct CommandCenterTab: View { value: self.gatewayAgentCountText, color: OpenClawBrand.accentHot) } - .padding(.vertical, 9) - .background { - RoundedRectangle(cornerRadius: 10, style: .continuous) - .fill(self.colorScheme == .dark ? Color.black.opacity(0.16) : Color.black.opacity(0.026)) - .overlay { - RoundedRectangle(cornerRadius: 10, style: .continuous) - .strokeBorder( - Color.primary.opacity(self.colorScheme == .dark ? 0.08 : 0.045), - lineWidth: 1) - } - } + .padding(.vertical, 7) } } .padding(.horizontal, OpenClawProMetric.pagePadding) @@ -215,10 +200,7 @@ struct CommandCenterTab: View { private var defaultChatSessionSection: some View { CommandPanel(padding: 12) { VStack(spacing: 10) { - self.cardHeader( - title: "Agent session", - value: nil, - color: OpenClawBrand.accent) + self.cardHeader(title: "Agent session") Button { self.open(.chat(nil)) @@ -233,10 +215,7 @@ struct CommandCenterTab: View { private var recentSessions: some View { CommandPanel(padding: 12) { VStack(spacing: 10) { - self.cardHeader( - title: "Recent sessions", - value: nil, - color: .secondary) + self.cardHeader(title: "Recent sessions") if self.recentSessionPreviewRows.isEmpty { CommandEmptyStateRow( @@ -276,64 +255,47 @@ struct CommandCenterTab: View { } } - private func cardHeader( - title: String, - value: String?, - color: Color, - icon: String? = nil, - badgeValue: String? = nil, - action: (() -> Void)? = nil) -> some View - { + private func cardHeader(title: String) -> some View { HStack(spacing: 8) { Text(title) .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) - if let badgeValue { - Text(badgeValue) - .font(.caption2.weight(.bold)) - .foregroundStyle(.white) - .padding(.horizontal, 6) - .padding(.vertical, 3) - .background(OpenClawBrand.accentHot, in: Capsule()) - } Spacer(minLength: 8) - if let value { - if let action { - Button(value, action: action) - .font(.caption.weight(.semibold)) - .foregroundStyle(color) - } else { - HStack(spacing: 4) { - if let icon { - Image(systemName: icon) - .font(.caption2.weight(.bold)) - } - Text(value) - } - .font(.caption.weight(.semibold)) - .foregroundStyle(color) - } - } } } private var gatewayConnected: Bool { - GatewayStatusBuilder.build(appModel: self.appModel) == .connected + self.gatewayDisplayState == .connected } - private var gatewayStateText: String { - guard !self.gatewayConnected else { return "Healthy" } - let status = self.appModel.gatewayDisplayStatusText.trimmingCharacters(in: .whitespacesAndNewlines) - let lowercased = status.lowercased() - if lowercased.contains("approval") { return "Approval" } - if lowercased.contains("reconnect") { return "Reconnecting" } - if lowercased.contains("connect") { return "Connecting" } - if lowercased.contains("idle") { return "Idle" } - return "Offline" + private var gatewayDisplayState: GatewayDisplayState { + GatewayStatusBuilder.build(appModel: self.appModel) + } + + private var gatewayConnectionText: String { + switch self.gatewayDisplayState { + case .connected: + "Online" + case .connecting: + "Connecting" + case .error: + "Attention" + case .disconnected: + "Offline" + } } private var gatewayStatusColor: Color { - self.gatewayConnected ? OpenClawBrand.ok : .secondary + switch self.gatewayDisplayState { + case .connected: + OpenClawBrand.ok + case .connecting: + OpenClawBrand.accent + case .error: + OpenClawBrand.warn + case .disconnected: + .secondary + } } private var gatewayAddressText: String { diff --git a/apps/ios/Sources/Design/IPadSidebarScreenChrome.swift b/apps/ios/Sources/Design/IPadSidebarScreenChrome.swift index df8cd21cfa4..4a3281b7c3c 100644 --- a/apps/ios/Sources/Design/IPadSidebarScreenChrome.swift +++ b/apps/ios/Sources/Design/IPadSidebarScreenChrome.swift @@ -58,7 +58,8 @@ struct IPadSidebarScreenChrome: View { Button(action: gatewayAction) { OpenClawGatewayCompactPill() } - .buttonStyle(.plain) + .buttonBorderShape(.capsule) + .openClawGlassButton() .accessibilityHint("Opens Settings / Gateway") } else { OpenClawGatewayCompactPill() diff --git a/apps/ios/Sources/Design/OpenClawDocsScreen.swift b/apps/ios/Sources/Design/OpenClawDocsScreen.swift index 70ed060f103..3c220068d99 100644 --- a/apps/ios/Sources/Design/OpenClawDocsScreen.swift +++ b/apps/ios/Sources/Design/OpenClawDocsScreen.swift @@ -55,7 +55,8 @@ struct OpenClawDocsScreen: View { Button(action: gatewayAction) { OpenClawGatewayCompactPill() } - .buttonStyle(.plain) + .buttonBorderShape(.capsule) + .openClawGlassButton() .accessibilityHint("Opens Settings / Gateway") } else { OpenClawGatewayCompactPill() diff --git a/apps/ios/Sources/Design/OpenClawProComponents.swift b/apps/ios/Sources/Design/OpenClawProComponents.swift index df8f4c5072a..5f362e9ada5 100644 --- a/apps/ios/Sources/Design/OpenClawProComponents.swift +++ b/apps/ios/Sources/Design/OpenClawProComponents.swift @@ -1,25 +1,17 @@ import SwiftUI enum OpenClawProMetric { - static let pagePadding: CGFloat = 18 - static let cardRadius: CGFloat = 10 - static let controlRadius: CGFloat = 8 + static let pagePadding: CGFloat = 16 + static let cardRadius: CGFloat = 16 + static let controlRadius: CGFloat = 12 + static let compactControlSize: CGFloat = 36 static let bottomScrollInset: CGFloat = 96 } struct OpenClawProBackground: View { - @Environment(\.colorScheme) private var colorScheme - var body: some View { - Color(uiColor: self.colorScheme == .dark ? .systemBackground : .systemGroupedBackground) + Color(uiColor: .systemGroupedBackground) .ignoresSafeArea() - .overlay(alignment: .top) { - if self.colorScheme == .light { - Color.white.opacity(0.22) - .frame(height: 140) - .ignoresSafeArea() - } - } } } @@ -83,76 +75,87 @@ private struct ProPanelBackground: View { .overlay { shape.strokeBorder(self.borderStyle, lineWidth: 1) } - .overlay { - if self.isProminent { - shape.strokeBorder( - OpenClawBrand.accent.opacity(self.colorScheme == .dark ? 0.12 : 0.07), - lineWidth: 1) - .padding(1) - } - } } private var fill: AnyShapeStyle { - let base = self.isProminent - ? Color(uiColor: .systemBackground) - : Color(uiColor: .secondarySystemGroupedBackground) - if let tint { - let gradient = LinearGradient( - colors: [ - base, - tint.opacity(self.colorScheme == .dark ? 0.08 : 0.045), - base, - ], - startPoint: .topLeading, - endPoint: .bottomTrailing) - return AnyShapeStyle(gradient) - } - return AnyShapeStyle(base) + let color = self.isProminent ? UIColor.systemBackground : UIColor.secondarySystemGroupedBackground + return AnyShapeStyle(Color(uiColor: color)) } private var borderStyle: AnyShapeStyle { - AnyShapeStyle(Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.26 : 0.30)) + if let tint { + return AnyShapeStyle(tint.opacity(self.isProminent ? 0.18 : 0.10)) + } + return AnyShapeStyle(Color(uiColor: .separator).opacity(self.colorScheme == .dark ? 0.22 : 0.12)) } } -private struct ProLightGlassModifier: ViewModifier { +private struct ProInsetSurfaceModifier: ViewModifier { @Environment(\.colorScheme) private var colorScheme + let tint: Color let radius: CGFloat func body(content: Content) -> some View { - if #available(iOS 26.0, *), self.colorScheme == .light { - content.glassEffect(.regular, in: .rect(cornerRadius: self.radius)) + let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous) + content.background { + shape + .fill(Color(uiColor: .tertiarySystemGroupedBackground)) + .overlay { + shape.strokeBorder( + self.tint.opacity(self.colorScheme == .dark ? 0.18 : 0.10), + lineWidth: 1) + } + } + } +} + +private struct OpenClawGlassButtonModifier: ViewModifier { + let prominent: Bool + let tint: Color? + + func body(content: Content) -> some View { + if #available(iOS 26.0, *) { + if self.prominent { + content + .buttonStyle(.glassProminent) + .tint(self.tint ?? OpenClawBrand.accent) + } else { + content + .buttonStyle(.glass) + .tint(self.tint) + } + } else if self.prominent { + content + .buttonStyle(.borderedProminent) + .tint(self.tint ?? OpenClawBrand.accent) + } else { + content + .buttonStyle(.bordered) + .tint(self.tint) + } + } +} + +private struct OpenClawTabBarBehaviorModifier: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 26.0, *) { + content.tabBarMinimizeBehavior(.onScrollDown) } else { content } } } -private struct ProGlassSurfaceModifier: ViewModifier { - @Environment(\.colorScheme) private var colorScheme - let fill: Color - let stroke: Color +private struct OpenClawGlassSurfaceModifier: ViewModifier { let radius: CGFloat - let isProminent: Bool - var interactive = false func body(content: Content) -> some View { - let shape = RoundedRectangle(cornerRadius: self.radius, style: .continuous) - let surfaced = content.background { - shape - .fill(self.fill) - .overlay { - shape.strokeBorder(self.stroke, lineWidth: self.isProminent ? 1.2 : 1) - } - } - - if #available(iOS 26.0, *), self.colorScheme == .light { - surfaced.glassEffect( - self.interactive ? .regular.interactive() : .regular, - in: .rect(cornerRadius: self.radius)) + if #available(iOS 26.0, *) { + content.glassEffect(.regular, in: .rect(cornerRadius: self.radius)) } else { - surfaced + content.background( + .regularMaterial, + in: RoundedRectangle(cornerRadius: self.radius, style: .continuous)) } } } @@ -169,19 +172,20 @@ extension View { isProminent: isProminent)) } - func proGlassSurface( - fill: Color, - stroke: Color, - radius: CGFloat, - isProminent: Bool = false, - interactive: Bool = false) -> some View - { - self.modifier(ProGlassSurfaceModifier( - fill: fill, - stroke: stroke, - radius: radius, - isProminent: isProminent, - interactive: interactive)) + func proInsetSurface(tint: Color, radius: CGFloat) -> some View { + self.modifier(ProInsetSurfaceModifier(tint: tint, radius: radius)) + } + + func openClawGlassButton(prominent: Bool = false, tint: Color? = nil) -> some View { + self.modifier(OpenClawGlassButtonModifier(prominent: prominent, tint: tint)) + } + + func openClawTabBarBehavior() -> some View { + self.modifier(OpenClawTabBarBehaviorModifier()) + } + + func openClawGlassSurface(radius: CGFloat = OpenClawProMetric.controlRadius) -> some View { + self.modifier(OpenClawGlassSurfaceModifier(radius: radius)) } } @@ -199,11 +203,12 @@ private struct ProPanelSurfaceModifier: ViewModifier { tint: self.tint, isProminent: self.isProminent) } - .modifier(ProLightGlassModifier(radius: self.radius)) .shadow( - color: self.colorScheme == .dark ? .black.opacity(0.22) : .black.opacity(0.028), - radius: self.isProminent ? 9 : 4, - y: self.isProminent ? 4 : 1) + color: self.isProminent + ? (self.colorScheme == .dark ? .black.opacity(0.14) : .black.opacity(0.045)) + : .clear, + radius: self.isProminent ? 5 : 0, + y: self.isProminent ? 2 : 0) } } @@ -253,11 +258,13 @@ struct OpenClawSidebarRevealButton: View { let button = Button(action: self.headerAction.action) { Image(systemName: self.headerAction.systemName) .font(.system(size: 16, weight: .semibold)) - .frame(width: 38, height: 38) + .frame( + width: OpenClawProMetric.compactControlSize, + height: OpenClawProMetric.compactControlSize) .contentShape(Rectangle()) } - .buttonStyle(.plain) - .foregroundStyle(OpenClawBrand.accent) + .buttonBorderShape(.circle) + .openClawGlassButton(tint: OpenClawBrand.accent) .accessibilityLabel(self.headerAction.accessibilityLabel) if let accessibilityIdentifier = self.headerAction.accessibilityIdentifier { @@ -277,9 +284,105 @@ struct OpenClawSidebarHeaderLeadingSlot: View { } } +struct OpenClawGlassControlGroup: View { + @ViewBuilder let content: Content + + var body: some View { + if #available(iOS 26.0, *) { + GlassEffectContainer(spacing: 8) { + self.content + } + } else { + self.content + } + } +} + +enum OpenClawNoticeDetail { + case accent(String) + case requestID(String) +} + +struct OpenClawNoticeBanner: View { + let icon: String + let title: String + let message: String + let ownerLabel: String + let tint: Color + var detail: OpenClawNoticeDetail? + var primaryActionTitle: String? + var onPrimaryAction: (() -> Void)? + var secondaryActionTitle: String? + var onSecondaryAction: (() -> Void)? + + var body: some View { + ProCard(tint: self.tint, padding: 14) { + VStack(alignment: .leading, spacing: 12) { + HStack(alignment: .top, spacing: 12) { + ProIconBadge(systemName: self.icon, color: self.tint) + + VStack(alignment: .leading, spacing: 6) { + HStack(alignment: .firstTextBaseline, spacing: 8) { + Text(self.title) + .font(.subheadline.weight(.semibold)) + .multilineTextAlignment(.leading) + Spacer(minLength: 0) + Text(self.ownerLabel) + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) + } + + Text(self.message) + .font(.footnote) + .foregroundStyle(.secondary) + .fixedSize(horizontal: false, vertical: true) + + self.detailView + } + } + + if self.onPrimaryAction != nil || self.onSecondaryAction != nil { + OpenClawGlassControlGroup { + HStack(spacing: 10) { + if let primaryActionTitle, let onPrimaryAction { + Button(primaryActionTitle, action: onPrimaryAction) + .openClawGlassButton(prominent: true) + .controlSize(.small) + } + if let secondaryActionTitle, let onSecondaryAction { + Button(secondaryActionTitle, action: onSecondaryAction) + .openClawGlassButton() + .controlSize(.small) + } + } + } + } + } + } + } + + @ViewBuilder + private var detailView: some View { + if let detail { + switch detail { + case let .accent(value): + Text(value) + .font(.caption.weight(.medium)) + .foregroundStyle(self.tint) + .fixedSize(horizontal: false, vertical: true) + case let .requestID(value): + Text("Request ID: \(value)") + .font(.system(.caption, design: .monospaced).weight(.medium)) + .foregroundStyle(.secondary) + .textSelection(.enabled) + } + } + } +} + struct OpenClawAdaptiveHeaderRow: View { let title: String - let subtitle: String + let subtitle: String? var titleFont: Font = .title3.weight(.semibold) var subtitleFont: Font = .subheadline var subtitleLineLimit: Int? = 2 @@ -288,7 +391,7 @@ struct OpenClawAdaptiveHeaderRow: View { init( title: String, - subtitle: String, + subtitle: String? = nil, titleFont: Font = .title3.weight(.semibold), subtitleFont: Font = .subheadline, subtitleLineLimit: Int? = 2, @@ -351,11 +454,13 @@ struct OpenClawAdaptiveHeaderRow: View { .lineLimit(2) .minimumScaleFactor(0.86) .fixedSize(horizontal: false, vertical: true) - Text(self.subtitle) - .font(self.subtitleFont) - .foregroundStyle(.secondary) - .lineLimit(self.subtitleLineLimit) - .fixedSize(horizontal: false, vertical: true) + if let subtitle, !subtitle.isEmpty { + Text(subtitle) + .font(self.subtitleFont) + .foregroundStyle(.secondary) + .lineLimit(self.subtitleLineLimit) + .fixedSize(horizontal: false, vertical: true) + } } } } @@ -422,47 +527,22 @@ struct ProProgressBar: View { } } -struct ProCapsule: View { - @Environment(\.colorScheme) private var colorScheme - let title: String - let color: Color - var icon: String? - - var body: some View { - HStack(spacing: 6) { - if let icon { - Image(systemName: icon) - .font(.caption.weight(.semibold)) - } - Text(self.title) - .font(.caption.weight(.semibold)) - .lineLimit(1) - .minimumScaleFactor(0.78) - } - .fixedSize(horizontal: true, vertical: false) - .foregroundStyle(self.color) - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background { - Capsule() - .fill(self.color.opacity(self.colorScheme == .dark ? 0.16 : 0.10)) - .overlay { - Capsule() - .strokeBorder(self.color.opacity(self.colorScheme == .dark ? 0.30 : 0.18), lineWidth: 1) - } - } - } -} - struct OpenClawGatewayCompactPill: View { @Environment(NodeAppModel.self) private var appModel var body: some View { - ProCapsule( - title: self.title, - color: self.color, - icon: self.icon) - .accessibilityLabel("Gateway \(self.title)") + HStack(spacing: 6) { + Image(systemName: self.icon) + .font(.caption.weight(.semibold)) + Text(self.title) + .font(.caption.weight(.semibold)) + .lineLimit(1) + } + .foregroundStyle(self.color) + .padding(.horizontal, 4) + .frame(minHeight: 30) + .fixedSize(horizontal: true, vertical: false) + .accessibilityLabel("Gateway \(self.title)") } private var title: String { @@ -536,10 +616,7 @@ struct ProMetricTile: View { } .padding(11) .frame(maxWidth: .infinity, alignment: .leading) - .proGlassSurface( - fill: self.colorScheme == .dark ? Color.white.opacity(0.04) : Color.white.opacity(0.52), - stroke: self.color.opacity(self.colorScheme == .dark ? 0.18 : 0.10), - radius: 16) + .proInsetSurface(tint: self.color, radius: OpenClawProMetric.controlRadius) } } diff --git a/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift b/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift index 043df0a7241..cc2ee0aca5f 100644 --- a/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift +++ b/apps/ios/Sources/Design/RootTabsPhoneControlHub.swift @@ -27,7 +27,7 @@ struct RootTabsPhoneControlHub: View { .safeAreaPadding(.bottom, self.bottomScrollInset) } .navigationTitle("Control") - .navigationBarTitleDisplayMode(.inline) + .navigationBarTitleDisplayMode(.large) .navigationDestination(for: RootTabs.SidebarDestination.self) { destination in self.detail(for: destination) .navigationBarBackButtonHidden(true) @@ -39,15 +39,19 @@ struct RootTabsPhoneControlHub: View { } } - @ViewBuilder private var headerCard: some View { - if self.isCompactHeight { - ProCard(padding: 8, radius: OpenClawProMetric.cardRadius) { + ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) { + Button { + self.openPhoneRootDestination(.gateway) + } label: { HStack(spacing: 12) { - OpenClawProMark(size: 24, shadowRadius: 3) - VStack(alignment: .leading, spacing: 3) { + OpenClawProMark( + size: self.isCompactHeight ? 28 : 34, + shadowRadius: self.isCompactHeight ? 3 : 5) + VStack(alignment: .leading, spacing: 4) { Text(self.sidebarActiveAgentTitle) - .font(.subheadline.weight(.semibold)) + .font(.headline) + .foregroundStyle(.primary) .lineLimit(1) Text(self.gatewayDisplayLabel) .font(.footnote) @@ -56,71 +60,29 @@ struct RootTabsPhoneControlHub: View { .truncationMode(.middle) } Spacer(minLength: 8) - ProValuePill(value: self.gatewayStateText, color: self.gatewayStateColor) - } - } - .padding(.horizontal, OpenClawProMetric.pagePadding) - } else { - ProCard(radius: OpenClawProMetric.cardRadius) { - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 12) { - OpenClawProMark(size: 32, shadowRadius: 4) - VStack(alignment: .leading, spacing: 3) { - Text(self.sidebarActiveAgentTitle) - .font(.headline) - .lineLimit(1) - Text(self.gatewayDisplayLabel) - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - Spacer(minLength: 8) - ProValuePill(value: self.gatewayStateText, color: self.gatewayStateColor) + HStack(spacing: 7) { + ProStatusDot(color: self.gatewayStateColor) + Text(self.gatewayStateText) + .font(.footnote.weight(.semibold)) + .foregroundStyle(self.gatewayStateColor) + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.secondary) } - - self.gatewayActionRow } + .padding(self.isCompactHeight ? 10 : 14) + .contentShape(Rectangle()) } - .padding(.horizontal, OpenClawProMetric.pagePadding) + .buttonStyle(.plain) + .accessibilityLabel("Gateway \(self.gatewayStateText)") + .accessibilityHint("Opens Settings / Gateway") } - } - - private var gatewayActionRow: some View { - Button { - self.openPhoneRootDestination(.gateway) - } label: { - HStack(spacing: 10) { - ProStatusDot(color: self.gatewayStateColor) - VStack(alignment: .leading, spacing: 2) { - Text(self.gatewayStateText) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.primary) - Text(self.gatewayDisplayLabel) - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - Spacer(minLength: 8) - Text(self.gatewayActionTitle) - .font(.footnote.weight(.semibold)) - .foregroundStyle(OpenClawBrand.accent) - Image(systemName: "chevron.right") - .font(.caption2.weight(.bold)) - .foregroundStyle(.secondary) - } - .padding(10) - .background(Color.primary.opacity(0.055), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) - } - .buttonStyle(.plain) - .accessibilityLabel("Gateway \(self.gatewayStateText)") - .accessibilityHint("Opens Settings / Gateway") + .padding(.horizontal, OpenClawProMetric.pagePadding) } private func groupSection(_ group: RootTabs.SidebarGroup) -> some View { VStack(alignment: .leading, spacing: self.isCompactHeight ? 6 : 8) { - ProSectionHeader(title: group.title.capitalized) + ProSectionHeader(title: group.title.capitalized, uppercase: false) ProCard(padding: 0, radius: OpenClawProMetric.cardRadius) { VStack(spacing: 0) { ForEach(Array(group.destinations.enumerated()), id: \.element.id) { index, destination in @@ -157,21 +119,15 @@ struct RootTabsPhoneControlHub: View { private func rowLabel(_ destination: RootTabs.SidebarDestination) -> some View { HStack(alignment: .center, spacing: 12) { ProIconBadge(systemName: destination.systemImage, color: .secondary) - VStack(alignment: .leading, spacing: 3) { - Text(destination.title) - .font(.subheadline.weight(.semibold)) - .foregroundStyle(.primary) - Text(destination.subtitle) - .font(.footnote) - .foregroundStyle(.secondary) - .lineLimit(1) - } + Text(destination.title) + .font(.subheadline.weight(.semibold)) + .foregroundStyle(.primary) Spacer(minLength: 8) Image(systemName: "chevron.right") .font(.caption2.weight(.bold)) .foregroundStyle(.secondary) } - .padding(.vertical, self.isCompactHeight ? 8 : 10) + .padding(.vertical, self.isCompactHeight ? 7 : 9) .padding(.horizontal, 14) .contentShape(Rectangle()) } @@ -310,19 +266,6 @@ struct RootTabsPhoneControlHub: View { } } - private var gatewayActionTitle: String { - switch GatewayStatusBuilder.build(appModel: self.appModel) { - case .connected: - "Manage" - case .connecting: - "Details" - case .error: - "Fix" - case .disconnected: - "Connect" - } - } - private var isCompactHeight: Bool { self.verticalSizeClass == .compact } diff --git a/apps/ios/Sources/Design/SettingsProTabSections.swift b/apps/ios/Sources/Design/SettingsProTabSections.swift index 96b86b8a50b..372eb8b636d 100644 --- a/apps/ios/Sources/Design/SettingsProTabSections.swift +++ b/apps/ios/Sources/Design/SettingsProTabSections.swift @@ -5,7 +5,6 @@ extension SettingsProTab { var settingsHeader: some View { OpenClawAdaptiveHeaderRow( title: "Settings", - subtitle: "Gateway, permissions, voice, and device controls.", titleFont: .title3.weight(.semibold), subtitleFont: .callout) { @@ -40,8 +39,8 @@ extension SettingsProTab { } var gatewaySection: some View { - VStack(alignment: .leading, spacing: 8) { - ProSectionHeader(title: "Gateway", uppercase: false) + // Keep this container boundary: copying a directly returned generic ProCard crashes SwiftUI on iOS 26. + VStack(alignment: .leading, spacing: 0) { ProCard(padding: 0, radius: SettingsLayout.cardRadius) { VStack(spacing: 0) { NavigationLink(value: SettingsRoute.gateway) { @@ -51,15 +50,6 @@ extension SettingsProTab { .contentShape(Rectangle()) } .buttonStyle(.plain) - Divider() - self.gatewayDetailRow(label: "Address", value: self.gatewayAddress) - Divider() - self.gatewayDetailRow(label: "Server", value: self.gatewayServer) - Divider() - self.gatewayDetailRow(label: "Agents", value: "\(self.appModel.gatewayAgents.count)") - Divider() - self.gatewayActions - .padding(14) } } .padding(.horizontal, OpenClawProMetric.pagePadding) @@ -73,11 +63,14 @@ extension SettingsProTab { color: self.gatewayStatusColor) VStack(alignment: .leading, spacing: 3) { - Text("Connection") + Text(self.gatewayServer) .font(.subheadline.weight(.semibold)) - Text(self.gatewayStatusDetail) + .lineLimit(1) + Text(self.gatewaySummaryDetail) .font(.caption) - .foregroundStyle(self.gatewayStatusColor) + .foregroundStyle(.secondary) + .lineLimit(1) + .truncationMode(.middle) } Spacer(minLength: 8) @@ -88,20 +81,10 @@ extension SettingsProTab { } } - func gatewayDetailRow(label: String, value: String) -> some View { - HStack { - Text(label) - .font(.caption) - .foregroundStyle(.secondary) - Spacer(minLength: 8) - Text(value) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(1) - .truncationMode(.middle) - } - .padding(.horizontal, 14) - .frame(height: 40) + var gatewaySummaryDetail: String { + let agentCount = self.appModel.gatewayAgents.count + let agents = agentCount == 1 ? "1 agent" : "\(agentCount) agents" + return "\(self.gatewayStatusDetail) • \(agents)" } var gatewayActions: some View { @@ -128,51 +111,70 @@ extension SettingsProTab { } var settingsListSection: some View { - VStack(spacing: 10) { - self.settingsListRow( - icon: "checkmark.shield.fill", - title: "Approvals", - detail: self.approvalsDetail, - route: .approvals, - color: self.pendingApproval == nil ? .secondary : OpenClawBrand.warn, - badgeValue: self.pendingApproval == nil ? nil : "1") - self.settingsListRow( - icon: "person.2", - title: "Permissions", - detail: self.permissionsDetail, - route: .permissions) - self.settingsListRow( - icon: "point.3.connected.trianglepath.dotted", - title: "Channels / Integrations", - detail: "Message routing and external channel clients.", - route: .channels) - self.settingsListRow( - icon: "waveform", - title: "Voice & Talk", - detail: self.voiceDetail, - route: .voice) - self.settingsListRow( - icon: "globe", - title: "Diagnostics", - detail: self.diagnosticsDetail, - route: .diagnostics) - self.settingsListRow( - icon: "hand.raised", - title: "Privacy", - detail: self.privacyDetail, - route: .privacy) - self.settingsListRow( - icon: "bell", - title: "Notifications", - detail: self.notificationStatusText, - route: .notifications) - self.settingsListRow( - icon: "info.circle", - title: "About", - detail: DeviceInfoHelper.openClawVersionString(), - route: .about) + VStack(alignment: .leading, spacing: 18) { + ProCard(padding: 0, radius: SettingsLayout.cardRadius) { + VStack(spacing: 0) { + self.settingsListRow( + icon: "checkmark.shield.fill", + title: "Approvals", + detail: self.approvalsDetail, + route: .approvals, + color: self.pendingApproval == nil ? .secondary : OpenClawBrand.warn, + badgeValue: self.pendingApproval == nil ? nil : "1") + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "person.2", + title: "Permissions", + detail: self.permissionsDetail, + route: .permissions) + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "point.3.connected.trianglepath.dotted", + title: "Channels", + detail: "Connected services and message routing", + route: .channels) + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "waveform", + title: "Voice & Talk", + detail: self.voiceDetail, + route: .voice) + } + } + .padding(.horizontal, OpenClawProMetric.pagePadding) + + VStack(alignment: .leading, spacing: 8) { + ProSectionHeader(title: "Device", uppercase: false) + ProCard(padding: 0, radius: SettingsLayout.cardRadius) { + VStack(spacing: 0) { + self.settingsListRow( + icon: "globe", + title: "Diagnostics", + detail: self.diagnosticsDetail, + route: .diagnostics) + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "hand.raised", + title: "Privacy", + detail: self.privacyDetail, + route: .privacy) + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "bell", + title: "Notifications", + detail: self.notificationStatusText, + route: .notifications) + Divider().padding(.leading, 58) + self.settingsListRow( + icon: "info.circle", + title: "About", + detail: DeviceInfoHelper.openClawVersionString(), + route: .about) + } + } + .padding(.horizontal, OpenClawProMetric.pagePadding) + } } - .padding(.horizontal, OpenClawProMetric.pagePadding) } func settingsListRow( @@ -202,9 +204,10 @@ extension SettingsProTab { .font(.caption.weight(.semibold)) .foregroundStyle(.secondary) } - .padding(12) + .padding(.horizontal, 14) + .padding(.vertical, 9) .frame(maxWidth: .infinity, minHeight: SettingsLayout.rowHeight, alignment: .leading) - .proPanelSurface(radius: SettingsLayout.cardRadius) + .contentShape(Rectangle()) } .buttonStyle(.plain) } @@ -612,15 +615,12 @@ extension SettingsProTab { .minimumScaleFactor(0.76) } .frame(maxWidth: .infinity) - .frame(height: 34) - .foregroundStyle(color) - .background(color.opacity(0.09), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) - .overlay { - RoundedRectangle(cornerRadius: 8, style: .continuous) - .strokeBorder(color.opacity(0.14)) - } + .frame(height: 32) } - .buttonStyle(.plain) + .buttonStyle(.bordered) + .buttonBorderShape(.roundedRectangle(radius: 8)) + .tint(color) + .controlSize(.small) .disabled(isBusy || isDisabled) } diff --git a/apps/ios/Sources/Design/SettingsProTabSupport.swift b/apps/ios/Sources/Design/SettingsProTabSupport.swift index 72eac7e1997..a2ce7e54f52 100644 --- a/apps/ios/Sources/Design/SettingsProTabSupport.swift +++ b/apps/ios/Sources/Design/SettingsProTabSupport.swift @@ -16,7 +16,7 @@ enum SettingsRoute: Hashable { } enum SettingsLayout { - static let cardRadius: CGFloat = 12 + static let cardRadius: CGFloat = OpenClawProMetric.cardRadius static let rowHeight: CGFloat = 58 } diff --git a/apps/ios/Sources/Design/TalkProTab.swift b/apps/ios/Sources/Design/TalkProTab.swift index 345fae9c9c8..cbdb98eb6e0 100644 --- a/apps/ios/Sources/Design/TalkProTab.swift +++ b/apps/ios/Sources/Design/TalkProTab.swift @@ -3,7 +3,6 @@ import SwiftUI struct TalkProTab: View { @Environment(NodeAppModel.self) private var appModel @AppStorage("talk.enabled") private var talkEnabled: Bool = false - @AppStorage(TalkSpeechLocale.storageKey) private var talkSpeechLocale: String = TalkSpeechLocale.automaticID @AppStorage(TalkDefaults.speakerphoneEnabledKey) private var talkSpeakerphoneEnabled: Bool = TalkDefaults.speakerphoneEnabledByDefault @AppStorage("talk.background.enabled") private var talkBackgroundEnabled: Bool = false @@ -85,7 +84,7 @@ struct TalkProTab: View { ZStack { CommandControlBackground() ScrollView { - VStack(alignment: .leading, spacing: 10) { + VStack(alignment: .leading, spacing: 12) { self.header if let fallbackIssue = self.fallbackIssue { TalkRuntimeIssueBanner( @@ -97,8 +96,6 @@ struct TalkProTab: View { .padding(.horizontal, OpenClawProMetric.pagePadding) } self.voiceHeroCard - self.conversationCard - self.voiceModeCard self.controlsCard } .padding(.top, 16) @@ -109,54 +106,30 @@ struct TalkProTab: View { } private var header: some View { - HStack(alignment: .center, spacing: 11) { + OpenClawAdaptiveHeaderRow( + title: "Talk", + subtitle: self.headerSubtitle, + titleFont: .system(size: 30, weight: .bold), + subtitleFont: .caption.weight(.medium), + subtitleLineLimit: 1) + { if let headerLeadingAction { OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction) } - OpenClawProMark(size: 31, shadowRadius: 9) - VStack(alignment: .leading, spacing: 2) { - Text("Talk") - .font(.system(size: 27, weight: .bold, design: .rounded)) - Text(self.headerSubtitle) - .font(.caption.weight(.medium)) - .foregroundStyle(.secondary) - .lineLimit(1) - } - Spacer(minLength: 8) - self.statusChip + } accessory: { + EmptyView() } .padding(.horizontal, OpenClawProMetric.pagePadding) } - private var statusChip: some View { - HStack(spacing: 5) { - Circle() - .fill(self.state.color) - .frame(width: 7, height: 7) - Text(self.state.chipText) - .font(.caption.weight(.bold)) - .foregroundStyle(self.state.color) - } - .padding(.horizontal, 10) - .padding(.vertical, 7) - .background { - Capsule(style: .continuous) - .fill(self.state.color.opacity(0.11)) - .overlay { - Capsule(style: .continuous) - .strokeBorder(self.state.color.opacity(0.22), lineWidth: 1) - } - } - } - private var voiceHeroCard: some View { - CommandPanel(tint: self.state.color, isProminent: true, padding: 16) { - VStack(alignment: .center, spacing: 16) { + CommandPanel(isProminent: true, padding: 16) { + VStack(alignment: .center, spacing: 14) { TalkProOrb( mode: self.state.waveformMode(micLevel: self.appModel.talkMode.micLevel), color: self.state.color, systemImage: self.state.icon) - .frame(height: 188) + .frame(height: 132) .accessibilityHidden(true) VStack(spacing: 5) { @@ -172,84 +145,20 @@ struct TalkProTab: View { Button(action: self.handlePrimaryAction) { Label(self.state.primaryButtonTitle, systemImage: self.state.primaryButtonIcon) .font(.subheadline.weight(.bold)) - .foregroundStyle(.white) .frame(maxWidth: .infinity) .frame(height: 50) - .background { - RoundedRectangle(cornerRadius: 14, style: .continuous) - .fill(self.state.primaryButtonFill) - .shadow(color: self.state.primaryButtonFill.opacity(0.22), radius: 18, y: 8) - } } - .buttonStyle(.plain) + .buttonBorderShape(.capsule) + .openClawGlassButton(prominent: true, tint: self.state.primaryButtonFill) .disabled(self.state.primaryAction == .waiting) } } .padding(.horizontal, OpenClawProMetric.pagePadding) } - private var conversationCard: some View { - CommandPanel(padding: 0) { - VStack(spacing: 0) { - self.cardHeader(title: "Conversation", value: self.state.chipText, color: self.state.color) - .padding(.horizontal, 12) - .padding(.top, 11) - .padding(.bottom, 3) - self.infoRow(icon: "person.crop.circle.fill", title: "Agent", value: self.appModel.chatAgentName) - Divider().padding(.leading, 54) - self.infoRow( - icon: "bubble.left.and.text.bubble.right.fill", - title: "Session", - value: self.appModel.chatSessionKey) - Divider().padding(.leading, 54) - self.infoRow(icon: self.state.icon, title: "Runtime", value: self.appModel.talkMode.statusText) - } - } - .padding(.horizontal, OpenClawProMetric.pagePadding) - } - - private var voiceModeCard: some View { - CommandPanel(padding: 0) { - VStack(spacing: 0) { - self.cardHeader( - title: "Voice mode", - value: "Settings ›", - color: OpenClawBrand.accent, - action: self.openVoiceSettings) - .padding(.horizontal, 12) - .padding(.top, 11) - .padding(.bottom, 3) - self.infoRow( - icon: "waveform", - title: "Configured", - value: self.appModel.talkMode.gatewayTalkVoiceModeTitle) - Divider().padding(.leading, 54) - self.infoRow( - icon: "waveform", - title: "Active now", - value: self.activeModeText) - Divider().padding(.leading, 54) - self.infoRow(icon: "antenna.radiowaves.left.and.right", title: "Transport", value: self.transportText) - if let issueText = self.talkIssueText { - Divider().padding(.leading, 54) - self.infoRow(icon: "exclamationmark.triangle.fill", title: "Last issue", value: issueText) - } - Divider().padding(.leading, 54) - self.infoRow(icon: "key.fill", title: "Permission", value: self.permissionText) - Divider().padding(.leading, 54) - self.infoRow(icon: "globe", title: "Speech language", value: self.speechLocaleText) - } - } - .padding(.horizontal, OpenClawProMetric.pagePadding) - } - private var controlsCard: some View { CommandPanel(padding: 0) { VStack(spacing: 0) { - self.cardHeader(title: "Controls", value: nil, color: .secondary) - .padding(.horizontal, 12) - .padding(.top, 11) - .padding(.bottom, 3) self.controlToggleRow("Speakerphone", isOn: self.talkSpeakerphoneBinding) Divider().padding(.leading, 14) self.controlToggleRow("Background listening", isOn: self.$talkBackgroundEnabled) @@ -291,55 +200,6 @@ struct TalkProTab: View { } } - private func cardHeader( - title: String, - value: String?, - color: Color, - action: (() -> Void)? = nil) -> some View - { - HStack(spacing: 8) { - Text(title) - .font(.subheadline.weight(.bold)) - Spacer(minLength: 8) - if let value { - if let action { - Button(value, action: action) - .font(.caption.weight(.semibold)) - .foregroundStyle(color) - } else { - Text(value) - .font(.caption.weight(.semibold)) - .foregroundStyle(color) - } - } - } - } - - private func infoRow(icon: String, title: String, value: String) -> some View { - HStack(spacing: 10) { - Image(systemName: icon) - .font(.caption.weight(.bold)) - .foregroundStyle(self.state.color) - .frame(width: 30, height: 30) - .background { - RoundedRectangle(cornerRadius: 8, style: .continuous) - .fill(self.state.color.opacity(0.11)) - } - VStack(alignment: .leading, spacing: 2) { - Text(title) - .font(.caption2.weight(.medium)) - .foregroundStyle(.secondary) - Text(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? "—" : value) - .font(.subheadline.weight(.semibold)) - .lineLimit(1) - .minimumScaleFactor(0.78) - } - Spacer(minLength: 0) - } - .padding(.horizontal, 12) - .padding(.vertical, 9) - } - private var gatewayConnected: Bool { !self.appModel.isAppleReviewDemoModeEnabled && GatewayStatusBuilder.build(appModel: self.appModel) == .connected @@ -372,41 +232,6 @@ struct TalkProTab: View { return "Routes voice to \(self.appModel.chatAgentName)." } - private var transportText: String { - let provider = self.appModel.talkMode.gatewayTalkProviderLabel.trimmingCharacters(in: .whitespacesAndNewlines) - let transport = self.appModel.talkMode.gatewayTalkTransportLabel.trimmingCharacters(in: .whitespacesAndNewlines) - if provider.isEmpty || provider == "Not loaded" { return transport.isEmpty ? "Not loaded" : transport } - if transport.isEmpty || transport == "Not loaded" { return provider } - return "\(provider) • \(transport)" - } - - private var activeModeText: String { - let title = self.appModel.talkMode.gatewayTalkActiveModeTitle.trimmingCharacters(in: .whitespacesAndNewlines) - let subtitle = (self.appModel.talkMode.gatewayTalkActiveModeSubtitle ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - if title.isEmpty { return "Not active" } - if subtitle.isEmpty { return title } - return "\(title) • \(subtitle)" - } - - private var talkIssueText: String? { - let text = (self.appModel.talkMode.gatewayTalkLastIssueText ?? "") - .trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - } - - private var permissionText: String { - if let failure = self.appModel.talkMode.gatewayTalkPermissionState.failureMessage { - return failure - } - return self.appModel.talkMode.gatewayTalkPermissionState.statusLabel - } - - private var speechLocaleText: String { - if self.talkSpeechLocale == TalkSpeechLocale.automaticID { return "Automatic" } - return self.talkSpeechLocale - } - private func alignPersistedTalkState() { if self.appModel.isAppleReviewDemoModeEnabled, self.talkEnabled || self.appModel.talkMode.isEnabled @@ -579,7 +404,7 @@ struct TalkProState: Equatable { return OpenClawBrand.warn default: if !self.isConfigLoaded { return OpenClawBrand.warn } - return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot + return self.isEnabled ? OpenClawBrand.ok : .secondary } } @@ -677,13 +502,13 @@ private struct TalkProOrb: View { } Circle() .fill(self.color.opacity(0.13)) - .frame(width: 128, height: 128) + .frame(width: 104, height: 104) .overlay { Circle() .strokeBorder(self.color.opacity(0.30), lineWidth: 1) } - TalkProWaveform(mode: self.mode, tint: self.color, barCount: 18) - .frame(width: 116, height: 52) + TalkProWaveform(mode: self.mode, tint: self.color, barCount: 12) + .frame(width: 92, height: 44) .opacity(self.showsWaveform ? 1 : 0) Image(systemName: self.systemImage) .font(.system(size: 34, weight: .bold)) diff --git a/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift b/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift index 1f425abcf6b..0e76464e610 100644 --- a/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift +++ b/apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift @@ -2,68 +2,22 @@ import SwiftUI import UIKit struct TalkRuntimeIssueBanner: View { - @Environment(\.colorScheme) private var colorScheme - let issue: TalkRuntimeIssue var onOpenSettings: (() -> Void)? var onShowDetails: (() -> Void)? var body: some View { - VStack(alignment: .leading, spacing: 10) { - HStack(alignment: .top, spacing: 10) { - Image(systemName: self.iconName) - .font(.headline.weight(.semibold)) - .foregroundStyle(self.tint) - .frame(width: 20) - .padding(.top, 2) - - VStack(alignment: .leading, spacing: 5) { - HStack(alignment: .firstTextBaseline, spacing: 8) { - Text(self.issue.fallbackBannerTitle) - .font(.subheadline.weight(.semibold)) - .multilineTextAlignment(.leading) - Spacer(minLength: 0) - Text(self.issue.fallbackBannerOwnerLabel) - .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) - } - - Text(self.issue.fallbackBannerMessage) - .font(.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - - Text(self.issue.displayMessage) - .font(.caption.weight(.medium)) - .foregroundStyle(self.tint) - .fixedSize(horizontal: false, vertical: true) - } - } - - HStack(spacing: 10) { - if let onOpenSettings { - Button("Open Settings", action: onOpenSettings) - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - if let onShowDetails { - Button("Details", action: onShowDetails) - .buttonStyle(.bordered) - .controlSize(.small) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(13) - .background { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(.ultraThickMaterial) - .overlay { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .strokeBorder(Color.primary.opacity(self.colorScheme == .dark ? 0.12 : 0.07), lineWidth: 1) - } - .shadow(color: .black.opacity(self.colorScheme == .dark ? 0.16 : 0.07), radius: 16, y: 7) - } + OpenClawNoticeBanner( + icon: self.iconName, + title: self.issue.fallbackBannerTitle, + message: self.issue.fallbackBannerMessage, + ownerLabel: self.issue.fallbackBannerOwnerLabel, + tint: self.tint, + detail: .accent(self.issue.displayMessage), + primaryActionTitle: "Open Settings", + onPrimaryAction: self.onOpenSettings, + secondaryActionTitle: "Details", + onSecondaryAction: self.onShowDetails) } private var iconName: String { diff --git a/apps/ios/Sources/Gateway/GatewayProblemView.swift b/apps/ios/Sources/Gateway/GatewayProblemView.swift index e18c0e4673c..820724f469e 100644 --- a/apps/ios/Sources/Gateway/GatewayProblemView.swift +++ b/apps/ios/Sources/Gateway/GatewayProblemView.swift @@ -3,71 +3,23 @@ import SwiftUI import UIKit struct GatewayProblemBanner: View { - @Environment(\.colorScheme) private var colorScheme - let problem: GatewayConnectionProblem var primaryActionTitle: String? var onPrimaryAction: (() -> Void)? var onShowDetails: (() -> Void)? var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack(alignment: .top, spacing: 10) { - Image(systemName: self.iconName) - .font(.headline.weight(.semibold)) - .foregroundStyle(self.tint) - .frame(width: 20) - .padding(.top, 2) - - VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .firstTextBaseline, spacing: 8) { - Text(self.problem.title) - .font(.subheadline.weight(.semibold)) - .multilineTextAlignment(.leading) - Spacer(minLength: 0) - Text(self.ownerLabel) - .font(.caption.weight(.semibold)) - .foregroundStyle(.secondary) - } - - Text(self.problem.message) - .font(.footnote) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - - if let requestId = self.problem.requestId { - Text("Request ID: \(requestId)") - .font(.system(.caption, design: .monospaced).weight(.medium)) - .foregroundStyle(.secondary) - .textSelection(.enabled) - } - } - } - - HStack(spacing: 10) { - if let primaryActionTitle, let onPrimaryAction { - Button(primaryActionTitle, action: onPrimaryAction) - .buttonStyle(.borderedProminent) - .controlSize(.small) - } - if let onShowDetails { - Button("Details", action: onShowDetails) - .buttonStyle(.bordered) - .controlSize(.small) - } - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .padding(14) - .background { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .fill(.ultraThickMaterial) - .overlay { - RoundedRectangle(cornerRadius: 16, style: .continuous) - .strokeBorder(Color.primary.opacity(self.colorScheme == .dark ? 0.12 : 0.07), lineWidth: 1) - } - .shadow(color: .black.opacity(self.colorScheme == .dark ? 0.18 : 0.08), radius: 18, y: 8) - } + OpenClawNoticeBanner( + icon: self.iconName, + title: self.problem.title, + message: self.problem.message, + ownerLabel: self.ownerLabel, + tint: self.tint, + detail: self.problem.requestId.map(OpenClawNoticeDetail.requestID), + primaryActionTitle: self.primaryActionTitle, + onPrimaryAction: self.onPrimaryAction, + secondaryActionTitle: "Details", + onSecondaryAction: self.onShowDetails) } private var iconName: String { diff --git a/apps/ios/Sources/RootTabs.swift b/apps/ios/Sources/RootTabs.swift index 961937842c8..2c8c63041a2 100644 --- a/apps/ios/Sources/RootTabs.swift +++ b/apps/ios/Sources/RootTabs.swift @@ -188,6 +188,7 @@ struct RootTabs: View { .tabItem { Label("Settings", systemImage: "gearshape.fill") } .tag(AppTab.settings) } + .openClawTabBarBehavior() } private var sidebarSplitContent: some View { @@ -402,7 +403,6 @@ struct RootTabs: View { ChatProTab( headerLeadingAction: self.sidebarHeaderLeadingAction, headerTitle: "Chat", - headerSubtitle: "Agent conversation", showsAgentBadge: false, ownsNavigationStack: false, openSettings: { self.selectSidebarDestination(.gateway) }) diff --git a/apps/ios/Sources/RootTabsNavigation.swift b/apps/ios/Sources/RootTabsNavigation.swift index 3686a9e2460..827fcdbd625 100644 --- a/apps/ios/Sources/RootTabsNavigation.swift +++ b/apps/ios/Sources/RootTabsNavigation.swift @@ -69,26 +69,6 @@ extension RootTabs { } } - var subtitle: String { - switch self { - case .chat: "Agent chat and recent work." - case .talk: "Realtime voice and fallback controls." - case .overview: "Status, entry points, health." - case .activity: "Gateway, session, and device activity." - case .agents: "Agent roster and readiness." - case .workboard: "Agent work queue and session handoff." - case .skillWorkshop: "Review and apply proposed skills." - case .instances: "Latest presence from OpenClaw nodes." - case .sessions: "Active sessions and defaults." - case .dreaming: "Memory signals and background synthesis." - case .usage: "API usage and costs." - case .cron: "Wakeups and recurring runs." - case .docs: "Reference docs and setup guides." - case .settings: "Connection, permissions, channels, and app options." - case .gateway: "Pairing, diagnostics, permissions, and device controls." - } - } - var systemImage: String { switch self { case .chat: "bubble.left" diff --git a/apps/ios/Sources/Status/VoiceWakeToast.swift b/apps/ios/Sources/Status/VoiceWakeToast.swift index b3f9e91c61a..651b458a33f 100644 --- a/apps/ios/Sources/Status/VoiceWakeToast.swift +++ b/apps/ios/Sources/Status/VoiceWakeToast.swift @@ -1,8 +1,6 @@ import SwiftUI struct VoiceWakeToast: View { - @Environment(\.colorScheme) private var colorScheme - var command: String var body: some View { @@ -19,10 +17,7 @@ struct VoiceWakeToast: View { } .padding(.vertical, 10) .padding(.horizontal, 12) - .proGlassSurface( - fill: self.colorScheme == .dark ? Color.white.opacity(0.055) : Color.white.opacity(0.72), - stroke: self.colorScheme == .dark ? Color.white.opacity(0.12) : Color.black.opacity(0.08), - radius: 14) + .openClawGlassSurface() .accessibilityLabel("Voice Wake triggered") .accessibilityValue("Command: \(self.command)") } diff --git a/apps/ios/Tests/RootTabsSourceGuardTests.swift b/apps/ios/Tests/RootTabsSourceGuardTests.swift index 5e45c42c4ef..2691c93ef45 100644 --- a/apps/ios/Tests/RootTabsSourceGuardTests.swift +++ b/apps/ios/Tests/RootTabsSourceGuardTests.swift @@ -174,7 +174,9 @@ struct RootTabsSourceGuardTests { let dreamingSource = try String(contentsOf: Self.agentProDreamingDestinationSourceURL(), encoding: .utf8) #expect(!source.contains("ToolbarItem")) - #expect(source.contains("self.directHeaderLeadingAction(for: route) == nil ? .visible : .hidden")) + #expect(source + .contains("route == .agents || self.directHeaderLeadingAction(for: route) != nil ? .hidden : .visible")) + #expect(destinationsSource.contains(".toolbar(.hidden, for: .navigationBar)")) #expect(destinationsSource.contains("self.directHeaderLeadingAction(for: .instances)")) #expect(destinationsSource.contains("self.directHeaderLeadingAction(for: .dreaming)")) #expect(destinationsSource.contains("self.directHeader(\n for: .usage")) @@ -184,6 +186,80 @@ struct RootTabsSourceGuardTests { #expect(dreamingSource.contains("OpenClawSidebarHeaderLeadingSlot(action: headerLeadingAction)")) } + @Test func `iOS 26 chrome uses native glass while content cards stay quiet`() throws { + let rootSource = try String(contentsOf: Self.rootTabsSourceURL(), encoding: .utf8) + let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8) + let cardSurface = try Self.extract( + componentsSource, + from: "private struct ProPanelSurfaceModifier: ViewModifier", + to: "struct ProIconBadge: View") + + #expect(rootSource.contains(".openClawTabBarBehavior()")) + #expect(componentsSource.contains("content.tabBarMinimizeBehavior(.onScrollDown)")) + #expect(componentsSource.contains(".buttonStyle(.glassProminent)")) + #expect(componentsSource.contains(".buttonStyle(.glass)")) + #expect(componentsSource.contains("GlassEffectContainer(spacing: 8)")) + #expect(componentsSource.contains("if #available(iOS 26.0, *)")) + #expect(componentsSource.contains(".buttonStyle(.borderedProminent)")) + #expect(componentsSource.contains(".buttonStyle(.bordered)")) + #expect(componentsSource.contains("struct OpenClawNoticeBanner: View")) + #expect(!cardSurface.contains("glassEffect")) + } + + @Test func `professional layout avoids nested pills and card stacks`() throws { + let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8) + let agentSource = try String(contentsOf: Self.agentProTabOverviewSourceURL(), encoding: .utf8) + let talkSource = try String(contentsOf: Self.talkProTabSourceURL(), encoding: .utf8) + let settingsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8) + let overviewSource = try String(contentsOf: Self.commandCenterSourceURL(), encoding: .utf8) + let overviewRowsSource = try String(contentsOf: Self.commandCenterSupportSourceURL(), encoding: .utf8) + let gatewayStatus = try Self.extract( + componentsSource, + from: "struct OpenClawGatewayCompactPill: View", + to: "struct ProMetricTile: View") + let agentFilters = try Self.extract( + agentSource, + from: "var agentFilters: some View", + to: "var agentFiltersActive: Bool") + let agentRow = try Self.extract( + agentSource, + from: "func agentRow(_ agent: AgentSummary) -> some View", + to: "func headerIconButton(") + let settingsList = try Self.extract( + settingsSource, + from: "var settingsListSection: some View", + to: "func settingsListRow(") + let settingsRow = try Self.extract( + settingsSource, + from: "func settingsListRow(", + to: "func destination(for route:") + + #expect(gatewayStatus.contains("HStack(spacing: 6)")) + #expect(!gatewayStatus.contains("ProCapsule(")) + #expect(!gatewayStatus.contains("Capsule()")) + #expect(agentFilters.contains("Picker(\"Agent status\"")) + #expect(agentFilters.contains(".pickerStyle(.segmented)")) + #expect(!agentFilters.contains(".openClawGlassButton(")) + #expect(!agentRow.contains("agentMetric")) + #expect(!agentRow.contains("chevron.right")) + #expect(agentRow.contains("Image(systemName: \"checkmark\")")) + #expect(agentRow.contains("agentAccessibilityLabel")) + #expect(!talkSource.contains("conversationCard")) + #expect(!talkSource.contains("voiceModeCard")) + #expect(!talkSource.contains("statusChip")) + #expect(!settingsList.contains("ProSectionHeader(title: \"OpenClaw\"")) + #expect(settingsList.contains("ProSectionHeader(title: \"Device\"")) + #expect(settingsList.matches(of: /ProCard\(padding: 0/).count == 2) + #expect(settingsRow.contains(".contentShape(Rectangle())")) + #expect(!overviewSource.contains("ProCapsule(")) + #expect(overviewSource.contains("value: self.gatewayConnectionText")) + #expect(overviewSource.contains("switch self.gatewayDisplayState")) + #expect(overviewSource.contains("case .connecting:")) + #expect(overviewSource.contains("case .error:")) + #expect(!overviewRowsSource.contains("private var rowFill")) + #expect(overviewRowsSource.matches(of: /.contentShape\(Rectangle\(\)\)/).count >= 2) + } + @Test func `routed headers use shared adaptive layout`() throws { let componentsSource = try String(contentsOf: Self.proComponentsSourceURL(), encoding: .utf8) let featureChromeSource = try String(contentsOf: Self.iPadSidebarScreenChromeSourceURL(), encoding: .utf8) @@ -257,7 +333,11 @@ struct RootTabsSourceGuardTests { @Test func `phone hub header stays task first`() throws { let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8) - #expect(source.contains("private var gatewayActionRow: some View")) + #expect(source.contains("private var headerCard: some View")) + #expect(source.contains(".accessibilityLabel(\"Gateway \\(self.gatewayStateText)\")")) + #expect(!source.contains("private var gatewayActionRow: some View")) + #expect(!source.contains("ProValuePill(value: self.gatewayStateText")) + #expect(!source.contains("destination.subtitle")) #expect(source.contains("self.openPhoneRootDestination(.gateway)")) #expect(source.contains("private var phoneDetailBackAction: OpenClawSidebarHeaderAction")) #expect(source.contains("accessibilityLabel: \"Back to Control\"")) @@ -272,7 +352,7 @@ struct RootTabsSourceGuardTests { #expect(!source.contains("private func metric(label:")) } - @Test func phoneHubClearsDetailPathBeforeRootTabHandoff() throws { + @Test func `phone hub clears detail path before root tab handoff`() throws { let source = try String(contentsOf: Self.phoneHubSourceURL(), encoding: .utf8) let handoff = try Self.extract( source, @@ -287,7 +367,7 @@ struct RootTabsSourceGuardTests { #expect(clearRange.lowerBound < openRange.lowerBound) } - @Test func workboardUsesRealGatewayMethods() throws { + @Test func `workboard uses real gateway methods`() throws { let source = try String(contentsOf: Self.iPadWorkboardScreenSourceURL(), encoding: .utf8) #expect(source.contains("workboard.cards.list")) @@ -514,6 +594,8 @@ struct RootTabsSourceGuardTests { #expect(chromeSource.contains("let gatewayAction: (() -> Void)?")) #expect(chromeSource.contains("private var gatewayPill: some View")) #expect(chromeSource.contains("Button(action: gatewayAction)")) + #expect(chromeSource.contains(".buttonBorderShape(.capsule)")) + #expect(chromeSource.contains(".openClawGlassButton()")) #expect(chromeSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")")) #expect(featureSource.matches(of: /gatewayAction: self\.openSettings/).count == 2) #expect(rootSource.contains("IPadActivityScreen(")) @@ -541,7 +623,7 @@ struct RootTabsSourceGuardTests { #expect(!rootSource.contains("showGatewayActions")) #expect(!rootSource.contains("gatewayActionsDialog")) #expect(overviewSource.contains("Button(action: self.openSettings)")) - #expect(overviewSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")")) + #expect(overviewSource.contains(".accessibilityHint(\"Opens gateway settings\")")) #expect(agentSource.contains("let openSettings: (() -> Void)?")) #expect(agentOverviewSource.contains("OpenClawGatewayCompactPill()")) #expect(agentOverviewSource.contains("Button(action: openSettings)")) @@ -550,7 +632,11 @@ struct RootTabsSourceGuardTests { .count >= 3) #expect(chatSource.contains("let openSettings: (() -> Void)?")) #expect(chatSource.contains("private var connectionPillButton: some View")) + #expect(chatSource.contains(".buttonBorderShape(.capsule)")) + #expect(chatSource.contains(".openClawGlassButton()")) #expect(docsSource.contains("let gatewayAction: (() -> Void)?")) + #expect(docsSource.contains(".buttonBorderShape(.capsule)")) + #expect(docsSource.contains(".openClawGlassButton()")) #expect(settingsSource.contains("NavigationLink(value: SettingsRoute.gateway)")) #expect(rootSource.contains("case .settings:")) #expect(rootSource @@ -589,7 +675,7 @@ struct RootTabsSourceGuardTests { #expect(notificationGuidanceSource.contains("suppressFuture: true")) #expect(notificationGuidanceSource.contains("Text(\"Don't show again\")")) #expect(rootSource.contains("private func selectSettingsRoute(_ route: SettingsRoute)")) - #expect(settingsSource.contains("title: \"Channels / Integrations\"")) + #expect(settingsSource.contains("title: \"Channels\"")) #expect(settingsSource.contains("route: .channels")) #expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")")) } @@ -638,7 +724,8 @@ struct RootTabsSourceGuardTests { #expect(actionsSource.contains("self.gatewayController.refreshActiveGatewayRegistrationFromSettings()")) #expect(actionsSource.contains("self.gatewayController.restartDiscovery()")) #expect(actionsSource.contains("await self.appModel.refreshGatewayOverviewIfConnected()")) - #expect(actionsSource.contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")")) + #expect(actionsSource + .contains("self.gatewayController.requestLocalNetworkAccess(reason: \"settings_preflight\")")) #expect(controllerSource.contains("await self.tcpReachabilityProbe(")) #expect(controllerSource.contains("Check Tailscale or LAN.")) #expect(actionsSource.contains("Tailscale is off on this device. Turn it on, then try again.")) @@ -710,7 +797,7 @@ struct RootTabsSourceGuardTests { #expect(chatSource.matches(of: /self\.appModel\.makeChatTransport\(\)/).count == 2) #expect(appModelSource.contains("return IOSGatewayChatTransport(gateway: self.operatorSession)")) - #expect(settingsSectionsSource.contains("Message routing and external channel clients.")) + #expect(settingsSectionsSource.contains("Connected services and message routing")) #expect(channelsSource.contains("\"clickclack\": SettingsChannelFallbackMetadata")) #expect(channelsSource.contains("label: \"ClickClack\"")) #expect(channelsSource.contains("Self-hosted chat bot routing.")) @@ -751,6 +838,13 @@ struct RootTabsSourceGuardTests { .appendingPathComponent("Sources/Design/CommandCenterTab.swift") } + private static func commandCenterSupportSourceURL() -> URL { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("Sources/Design/CommandCenterSupport.swift") + } + private static func agentProTabSourceURL() -> URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() @@ -852,6 +946,13 @@ struct RootTabsSourceGuardTests { .appendingPathComponent("Sources/Design/ChatProTab.swift") } + private static func talkProTabSourceURL() -> URL { + URL(fileURLWithPath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .appendingPathComponent("Sources/Design/TalkProTab.swift") + } + private static func docsSourceURL() -> URL { URL(fileURLWithPath: #filePath) .deletingLastPathComponent() diff --git a/apps/ios/UITests/OpenClawSnapshotUITests.swift b/apps/ios/UITests/OpenClawSnapshotUITests.swift index e6cac10afca..4db454394ab 100644 --- a/apps/ios/UITests/OpenClawSnapshotUITests.swift +++ b/apps/ios/UITests/OpenClawSnapshotUITests.swift @@ -30,15 +30,15 @@ final class OpenClawSnapshotUITests: XCTestCase { } override func tearDownWithError() throws { - app?.terminate() - app = nil + self.app?.terminate() + self.app = nil try super.tearDownWithError() } func testConnectedGatewayTabs() { for appearance in ScreenshotAppearance.allCases { for target in Self.screenshotTargets { - launchApp(for: target, appearance: appearance) + self.launchApp(for: target, appearance: appearance) let name = appearance == .light ? target.name : "\(target.name)-dark" snapshot(name, timeWaitingForIdle: 5) } @@ -47,26 +47,50 @@ final class OpenClawSnapshotUITests: XCTestCase { func testControlOverviewNavigation() throws { try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only") - launchApp(for: ScreenshotTarget( + self.launchApp(for: ScreenshotTarget( initialTab: "control", initialDestination: "control", - name: "control-overview-navigation" - )) + name: "control-overview-navigation")) - let overview = app?.buttons.containing(.staticText, identifier: "Overview").firstMatch + let overview = self.app?.buttons.containing(.staticText, identifier: "Overview").firstMatch XCTAssertTrue(overview?.waitForExistence(timeout: 5) == true) overview?.tap() - XCTAssertTrue(app?.buttons["Back to Control"].waitForExistence(timeout: 5) == true) - XCTAssertEqual(app?.state, .runningForeground) + XCTAssertTrue(self.app?.buttons["Back to Control"].waitForExistence(timeout: 5) == true) + XCTAssertEqual(self.app?.state, .runningForeground) + } + + func testChatComposerStartsCompactAndGrowsWithDraft() throws { + try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone composer proof only") + self.launchApp(for: ScreenshotTarget( + initialTab: "chat", + initialDestination: "chat", + name: "chat-composer-growth")) + + let textField = try XCTUnwrap(app?.textFields.firstMatch) + XCTAssertTrue(textField.waitForExistence(timeout: 8)) + let compactHeight = textField.frame.height + XCTAssertLessThanOrEqual(compactHeight, 44) + self.attachScreenshot(named: "chat-composer-compact") + + textField.tap() + textField.typeText( + "Draft a polished launch note that covers the new design, validation, rollout plan, and follow-up details for the team.") + let composerGrew = expectation( + for: NSPredicate { _, _ in textField.frame.height >= compactHeight + 12 }, + evaluatedWith: textField) + wait(for: [composerGrew], timeout: 4) + self.attachScreenshot(named: "chat-composer-expanded") + + self.app?.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.2)).tap() + XCTAssertTrue(self.app?.keyboards.firstMatch.waitForNonExistence(timeout: 3) == true) } func testLiveGatewayControlOverviewNavigation() throws { try XCTSkipIf(UIDevice.current.userInterfaceIdiom != .phone, "Phone control hub only") try XCTSkipUnless( ProcessInfo.processInfo.environment["OPENCLAW_IOS_LIVE_GATEWAY"] == "1", - "Set OPENCLAW_IOS_LIVE_GATEWAY=1 and copy a fresh setup code to the simulator pasteboard" - ) + "Set OPENCLAW_IOS_LIVE_GATEWAY=1 and copy a fresh setup code to the simulator pasteboard") let app = XCUIApplication() addUIInterruptionMonitor(withDescription: "Local network access") { alert in @@ -104,10 +128,10 @@ final class OpenClawSnapshotUITests: XCTestCase { let overview = app.buttons.containing(.staticText, identifier: "Overview").firstMatch XCTAssertTrue(overview.waitForExistence(timeout: 8)) - attachScreenshot(named: "live-gateway-control") + self.attachScreenshot(named: "live-gateway-control") overview.tap() XCTAssertTrue(app.buttons["Back to Control"].waitForExistence(timeout: 8)) - attachScreenshot(named: "live-gateway-overview") + self.attachScreenshot(named: "live-gateway-overview") XCTAssertEqual(app.state, .runningForeground) } @@ -118,7 +142,7 @@ final class OpenClawSnapshotUITests: XCTestCase { self.app?.terminate() let app = XCUIApplication() - setupSnapshot(app, waitForAnimations: false) + setupSnapshot(app) app.launchArguments += [ "--openclaw-screenshot-mode", "--openclaw-appearance", @@ -137,7 +161,7 @@ final class OpenClawSnapshotUITests: XCTestCase { } private func attachScreenshot(named name: String) { - guard let app = app else { return } + guard let app else { return } let attachment = XCTAttachment(screenshot: app.screenshot()) attachment.name = name attachment.lifetime = .keepAlways diff --git a/apps/ios/project.yml b/apps/ios/project.yml index c563cb0328b..dc3cf3b4a2e 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -3,7 +3,7 @@ options: bundleIdPrefix: ai.openclawfoundation deploymentTarget: iOS: "18.0" - xcodeVersion: "16.0" + xcodeVersion: "26.0" settings: base: diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift index b1dbfbf5797..253a8128d65 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatComposer.swift @@ -333,11 +333,12 @@ struct OpenClawChatComposer: View { private var cleanEditor: some View { VStack(alignment: .leading, spacing: 6) { - HStack(alignment: .center, spacing: 8) { + HStack(alignment: .bottom, spacing: 8) { self.compactAccessory(self.attachmentPicker) - HStack(alignment: .center, spacing: 8) { + HStack(alignment: .bottom, spacing: 8) { self.editorOverlay + .padding(.vertical, self.cleanEditorTextPadding) .frame(minHeight: self.cleanEditorMinHeight) if let talkControl { @@ -348,10 +349,10 @@ struct OpenClawChatComposer: View { .padding(.trailing, 6) .frame(minHeight: self.cleanEditorMinHeight) .background( - Capsule(style: .continuous) + RoundedRectangle(cornerRadius: self.cleanEditorCornerRadius, style: .continuous) .fill(OpenClawChatTheme.composerField) .overlay( - Capsule(style: .continuous) + RoundedRectangle(cornerRadius: self.cleanEditorCornerRadius, style: .continuous) .strokeBorder(OpenClawChatTheme.composerBorder))) self.sendButton @@ -516,16 +517,13 @@ struct OpenClawChatComposer: View { text: self.$viewModel.input, axis: .vertical) .font(.body) + .textFieldStyle(.plain) .lineLimit(1...4) + .fixedSize(horizontal: false, vertical: true) .submitLabel(.send) .onSubmit { self.sendDraftIfEnabled() } - .frame( - minHeight: self.textMinHeight, - idealHeight: self.textMinHeight, - maxHeight: self.textMaxHeight, - alignment: self.editorTextAlignment) .padding(.horizontal, self.cleanFieldTextInset) .padding(.vertical, self.composerChrome == .clean ? 0 : 6) .focused(self.$isFocused) @@ -637,6 +635,14 @@ struct OpenClawChatComposer: View { max(self.cleanControlHeight, self.textMinHeight) } + private var cleanEditorCornerRadius: CGFloat { + self.cleanControlHeight / 2 + } + + private var cleanEditorTextPadding: CGFloat { + 6 + } + private var sendButtonSize: CGFloat { self.composerChrome == .clean ? self.cleanControlHeight : 44 } @@ -661,10 +667,6 @@ struct OpenClawChatComposer: View { self.composerChrome == .clean ? .leading : .topLeading } - private var editorTextAlignment: Alignment { - self.composerChrome == .clean ? .leading : .top - } - private var sendButtonFill: Color { self.userAccent ?? OpenClawChatTheme.userBubble } diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatDynamicTypeSourceGuardTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatDynamicTypeSourceGuardTests.swift index 4c391c7840b..13e3ffc96ba 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatDynamicTypeSourceGuardTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatDynamicTypeSourceGuardTests.swift @@ -12,6 +12,9 @@ struct ChatDynamicTypeSourceGuardTests { #expect(!sources.composer.contains(".frame(height: self.cleanControlHeight)")) #expect(!sources.composer.contains("return self.composerChrome == .clean ? 48 : 64")) #expect(sources.composer.contains("@ScaledMetric(relativeTo: .body)")) + #expect(sources.composer.contains(".textFieldStyle(.plain)")) + #expect(sources.composer.contains(".lineLimit(1...4)")) + #expect(sources.composer.contains(".fixedSize(horizontal: false, vertical: true)")) } private static func scopedChatTextSources() throws -> ( diff --git a/scripts/native-app-i18n.ts b/scripts/native-app-i18n.ts index 6326dd5f53f..1eb6a35110d 100644 --- a/scripts/native-app-i18n.ts +++ b/scripts/native-app-i18n.ts @@ -123,7 +123,8 @@ const CONDITIONAL_BRANCHES = [ /\bif\s*\([^)]*\)\s*"((?:\\.|[^"\\])*)"\s*else\s*"((?:\\.|[^"\\])*)"/gu, /\?\s*"((?:\\.|[^"\\])*)"\s*:\s*"((?:\\.|[^"\\])*)"/gu, ]; -const UI_STRING_NAME_RE = /(?:title|subtitle|body|message|label|text|description|prompt|help)$/iu; +const UI_STRING_NAME_RE = + /(?:title|subtitle|body|message|label|text|description|detail|prompt|help)$/iu; const APPLE_STRING_PROPERTY = /\bvar\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*String\s*\{/gu; const APPLE_SWITCH_BRANCH = /(?:\bcase\b[^:\n]+|\bdefault)\s*:\s*(?:return\s+)?"((?:\\.|[^"\\])*)"/gu; @@ -135,7 +136,7 @@ const ANDROID_RESOURCE_COLLECTIONS = /<(?:string-array|plurals)\b[^>]*>([\s\S]*?)<\/(?:string-array|plurals)>/gu; const ANDROID_RESOURCE_ITEMS = /]*>([\s\S]*?)<\/item>/gu; const APPLE_NAMED_LITERALS = - /\b(?:title|subtitle|label|message|text|prompt|description|help)\s*:\s*(?:"""([\s\S]*?)"""|"((?:\\.|[^"\\])*)")/gu; + /\b([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(?:"""([\s\S]*?)"""|"((?:\\.|[^"\\])*)")/gu; const APPLE_VIEW_TYPE = /\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)[^:{\n]*:\s*[^{\n]*\bView\b/gu; const APPLE_VIEW_FUNCTION = /\bfunc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\([^{}]*?\)\s*(?:async\s*)?(?:throws\s*)?->\s*some\s+View\b/gu; @@ -622,12 +623,18 @@ function extractCandidates( } } for (const match of source.matchAll(APPLE_NAMED_LITERALS)) { + const argumentName = match[1]; const callName = enclosingCallName(source, match.index ?? 0); - if (!callName || !uiCallNames.has(callName)) { + if ( + !argumentName || + !UI_STRING_NAME_RE.test(argumentName) || + !callName || + !uiCallNames.has(callName) + ) { continue; } - const multiline = match[1]; - const literal = multiline ?? match[2]; + const multiline = match[2]; + const literal = multiline ?? match[3]; if (literal) { addCandidate( entries, diff --git a/test/scripts/native-app-i18n.test.ts b/test/scripts/native-app-i18n.test.ts index dd1341f232b..17501106c82 100644 --- a/test/scripts/native-app-i18n.test.ts +++ b/test/scripts/native-app-i18n.test.ts @@ -39,7 +39,7 @@ describe("native app i18n inventory", () => { ), ).toBe(true); expect(entries.some((entry) => entry.source === "QR Scanner Unavailable")).toBe(true); - expect(entries.some((entry) => entry.source === "Request ID: \\(requestId)")).toBe(true); + expect(entries.some((entry) => entry.source === "Request ID: \\(value)")).toBe(true); expect(entries.some((entry) => entry.source === "Open ${row.title}")).toBe(true); expect(entries.some((entry) => entry.source === "$deviceModel · $appVersion")).toBe(true); expect(entries.some((entry) => entry.source === "Approval command copied")).toBe(true); @@ -54,8 +54,25 @@ describe("native app i18n inventory", () => { expect(entries.some((entry) => entry.source === "DIARY")).toBe(true); expect(entries.some((entry) => entry.source === "ask OpenClaw $prompt")).toBe(true); expect(entries.some((entry) => entry.source === "OpenClaw is paused")).toBe(true); - expect(entries.some((entry) => entry.source === "Last issue")).toBe(true); - expect(entries.some((entry) => entry.source === "Agent chat and recent work.")).toBe(true); + expect(entries.some((entry) => entry.source === "Connected services and message routing")).toBe( + true, + ); + expect( + entries.some( + (entry) => + entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" && + entry.kind === "ui-named-argument" && + entry.source === "Details", + ), + ).toBe(true); + expect( + entries.some( + (entry) => + entry.path === "apps/ios/Sources/Design/TalkRuntimeIssueBanner.swift" && + entry.kind === "ui-named-argument" && + entry.source === "Open Settings", + ), + ).toBe(true); expect(entries.some((entry) => entry.source === "No sessions yet")).toBe(true); expect(entries.some((entry) => entry.source === "Don’t show this again")).toBe(true); expect(entries.some((entry) => entry.source === "Use Manual Gateway")).toBe(true);