diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 4351a3ebdac2..ade8ad8bc94f 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -899,7 +899,7 @@ }, { "kind": "ui-call", - "line": 2319, + "line": 2293, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This automation already has a queued run.", "surface": "android", @@ -907,7 +907,7 @@ }, { "kind": "ui-call", - "line": 2349, + "line": 2323, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation run queued.", "surface": "android", @@ -915,7 +915,7 @@ }, { "kind": "ui-call", - "line": 2349, + "line": 2323, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation started.", "surface": "android", @@ -923,7 +923,7 @@ }, { "kind": "ui-call", - "line": 2362, + "line": 2336, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway rejected the automation run.", "surface": "android", @@ -931,7 +931,7 @@ }, { "kind": "ui-call", - "line": 2393, + "line": 2367, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation enabled.", "surface": "android", @@ -939,7 +939,7 @@ }, { "kind": "ui-call", - "line": 2393, + "line": 2367, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation paused.", "surface": "android", @@ -947,7 +947,7 @@ }, { "kind": "ui-call", - "line": 2415, + "line": 2389, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This automation changed on the gateway. Review the latest version before saving again.", "surface": "android", @@ -955,7 +955,7 @@ }, { "kind": "ui-call", - "line": 2421, + "line": 2395, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation updated.", "surface": "android", @@ -963,7 +963,7 @@ }, { "kind": "ui-call", - "line": 2436, + "line": 2410, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Automation deleted.", "surface": "android", @@ -971,7 +971,7 @@ }, { "kind": "ui-call", - "line": 2686, + "line": 2625, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Node offline. Reconnect and retry.", "surface": "android", @@ -979,7 +979,7 @@ }, { "kind": "ui-named-argument-concatenated", - "line": 2697, + "line": 2636, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Restore canvas now for session=$sessionKey source=$source. If existing A2UI state exists, replay it immediately. If not, create and render a compact mobile-friendly dashboard in Canvas.", "surface": "android", @@ -987,7 +987,7 @@ }, { "kind": "ui-call", - "line": 2718, + "line": 2657, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Failed to request restore. Tap to retry.", "surface": "android", @@ -995,7 +995,7 @@ }, { "kind": "ui-call", - "line": 2729, + "line": 2668, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "No canvas update yet. Tap to retry.", "surface": "android", @@ -1003,7 +1003,7 @@ }, { "kind": "ui-call", - "line": 3824, + "line": 3763, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect to a Gateway to save wake words", "surface": "android", @@ -1011,7 +1011,7 @@ }, { "kind": "ui-call", - "line": 3852, + "line": 3791, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Wake words saved", "surface": "android", @@ -1019,7 +1019,7 @@ }, { "kind": "ui-call", - "line": 3861, + "line": 3800, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not save wake words", "surface": "android", @@ -1027,7 +1027,7 @@ }, { "kind": "ui-call", - "line": 4572, + "line": 4511, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Failed: no secure gateway endpoint was detected. Enable gateway TLS or Tailscale Serve, or use a trusted private LAN address with Unencrypted selected.", "surface": "android", @@ -1035,7 +1035,7 @@ }, { "kind": "ui-call", - "line": 4576, + "line": 4515, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Failed: secure endpoint reached, but TLS fingerprint verification timed out. Check Tailscale Serve or gateway TLS and retry.", "surface": "android", @@ -1043,7 +1043,7 @@ }, { "kind": "ui-call", - "line": 4580, + "line": 4519, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Failed: couldn't reach the secure gateway endpoint for this host.", "surface": "android", @@ -1051,7 +1051,7 @@ }, { "kind": "ui-named-argument", - "line": 4630, + "line": 4569, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connecting…", "surface": "android", @@ -1059,7 +1059,7 @@ }, { "kind": "ui-named-argument", - "line": 4834, + "line": 4773, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Offline", "surface": "android", @@ -1067,7 +1067,7 @@ }, { "kind": "ui-call", - "line": 5609, + "line": 5578, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load provider catalog.", "surface": "android", @@ -1075,7 +1075,7 @@ }, { "kind": "ui-call", - "line": 5640, + "line": 5609, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Update your Gateway to view provider model config.", "surface": "android", @@ -1083,7 +1083,7 @@ }, { "kind": "ui-call", - "line": 5642, + "line": 5611, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load provider model config.", "surface": "android", @@ -1091,7 +1091,7 @@ }, { "kind": "ui-call", - "line": 5658, + "line": 5627, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Provider models loaded, but readiness is unavailable.", "surface": "android", @@ -1099,7 +1099,7 @@ }, { "kind": "ui-call", - "line": 5745, + "line": 5714, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automations.", "surface": "android", @@ -1107,7 +1107,7 @@ }, { "kind": "ui-call", - "line": 5831, + "line": 5800, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect automations.", "surface": "android", @@ -1115,7 +1115,7 @@ }, { "kind": "ui-call", - "line": 5841, + "line": 5810, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway returned an invalid automation.", "surface": "android", @@ -1123,7 +1123,7 @@ }, { "kind": "ui-call", - "line": 5845, + "line": 5814, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automation.", "surface": "android", @@ -1131,7 +1131,7 @@ }, { "kind": "ui-call", - "line": 5857, + "line": 5826, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect automation run history.", "surface": "android", @@ -1139,7 +1139,7 @@ }, { "kind": "ui-call", - "line": 5888, + "line": 5857, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automation run history.", "surface": "android", @@ -1147,7 +1147,7 @@ }, { "kind": "ui-call", - "line": 5905, + "line": 5874, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron changes require operator.admin access.", "surface": "android", @@ -1155,7 +1155,7 @@ }, { "kind": "ui-call", - "line": 5914, + "line": 5883, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to manage automations.", "surface": "android", @@ -1163,7 +1163,7 @@ }, { "kind": "ui-call", - "line": 5926, + "line": 5895, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Another cron action is still finishing.", "surface": "android", @@ -1171,7 +1171,7 @@ }, { "kind": "ui-call", - "line": 5973, + "line": 5942, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron action failed.", "surface": "android", @@ -1179,7 +1179,7 @@ }, { "kind": "ui-call", - "line": 6100, + "line": 6054, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load usage.", "surface": "android", @@ -1187,7 +1187,7 @@ }, { "kind": "ui-call", - "line": 6135, + "line": 6069, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load skills.", "surface": "android", @@ -1195,7 +1195,7 @@ }, { "kind": "ui-call", - "line": 6148, + "line": 6089, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to update skills.", "surface": "android", @@ -1203,7 +1203,7 @@ }, { "kind": "ui-call", - "line": 6152, + "line": 6093, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This gateway connection needs operator.admin to update skills.", "surface": "android", @@ -1211,7 +1211,7 @@ }, { "kind": "conditional-branch", - "line": 6167, + "line": 6108, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not disable skill.", "surface": "android", @@ -1219,7 +1219,7 @@ }, { "kind": "conditional-branch", - "line": 6167, + "line": 6108, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not enable skill.", "surface": "android", @@ -1227,7 +1227,7 @@ }, { "kind": "ui-call", - "line": 6185, + "line": 6126, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to search ClawHub skills.", "surface": "android", @@ -1235,7 +1235,7 @@ }, { "kind": "ui-call", - "line": 6231, + "line": 6172, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not search ClawHub skills.", "surface": "android", @@ -1243,7 +1243,7 @@ }, { "kind": "ui-call", - "line": 6244, + "line": 6185, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect ClawHub skills.", "surface": "android", @@ -1251,7 +1251,7 @@ }, { "kind": "conditional-branch", - "line": 6277, + "line": 6218, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "ClawHub did not return an installable version for ${skill.slug}.", "surface": "android", @@ -1259,7 +1259,7 @@ }, { "kind": "ui-call", - "line": 6293, + "line": 6234, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load ClawHub details for ${skill.slug}.", "surface": "android", @@ -1267,7 +1267,7 @@ }, { "kind": "ui-call", - "line": 6309, + "line": 6250, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to install ClawHub skills.", "surface": "android", @@ -1275,7 +1275,7 @@ }, { "kind": "ui-call", - "line": 6325, + "line": 6266, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This gateway connection needs operator.admin to install ClawHub skills.", "surface": "android", @@ -1283,7 +1283,7 @@ }, { "kind": "conditional-branch", - "line": 6415, + "line": 6356, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Installed $slug.", "surface": "android", @@ -1291,7 +1291,7 @@ }, { "kind": "ui-call", - "line": 6422, + "line": 6363, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not install ${slug} from ClawHub.", "surface": "android", @@ -1299,7 +1299,7 @@ }, { "kind": "ui-call", - "line": 6462, + "line": 6403, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to load Skill Workshop proposals.", "surface": "android", @@ -1307,7 +1307,7 @@ }, { "kind": "ui-call", - "line": 6502, + "line": 6443, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load Skill Workshop proposals.", "surface": "android", @@ -1315,7 +1315,7 @@ }, { "kind": "ui-call", - "line": 6522, + "line": 6463, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect Skill Workshop proposals.", "surface": "android", @@ -1323,7 +1323,7 @@ }, { "kind": "ui-call", - "line": 6574, + "line": 6515, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not inspect Skill Workshop proposal.", "surface": "android", @@ -1331,7 +1331,7 @@ }, { "kind": "ui-call", - "line": 6594, + "line": 6535, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Skill Workshop proposal actions require operator.admin scope.", "surface": "android", @@ -1339,7 +1339,7 @@ }, { "kind": "ui-call", - "line": 6599, + "line": 6540, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to update Skill Workshop proposals.", "surface": "android", @@ -1347,7 +1347,7 @@ }, { "kind": "ui-call", - "line": 6789, + "line": 6730, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not verify the device pairing change. Refresh and try again.", "surface": "android", @@ -1355,7 +1355,7 @@ }, { "kind": "ui-named-argument", - "line": 6871, + "line": 6812, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connected", "surface": "android", @@ -1363,7 +1363,7 @@ }, { "kind": "ui-call", - "line": 6901, + "line": 6842, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load nodes and devices.", "surface": "android", @@ -1371,7 +1371,7 @@ }, { "kind": "ui-call", - "line": 7568, + "line": 7492, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load channels.", "surface": "android", @@ -1379,7 +1379,7 @@ }, { "kind": "ui-call", - "line": 7598, + "line": 7510, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load dreaming.", "surface": "android", @@ -1387,7 +1387,7 @@ }, { "kind": "ui-call", - "line": 7635, + "line": 7525, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load gateway logs.", "surface": "android", @@ -1395,7 +1395,7 @@ }, { "kind": "ui-call", - "line": 8104, + "line": 8008, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "One time", "surface": "android", @@ -1403,7 +1403,7 @@ }, { "kind": "ui-call", - "line": 8113, + "line": 8017, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron", "surface": "android", @@ -1411,7 +1411,7 @@ }, { "kind": "ui-call", - "line": 8114, + "line": 8018, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Scheduled", "surface": "android", @@ -1419,7 +1419,7 @@ }, { "kind": "ui-call", - "line": 8122, + "line": 8026, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${days}d", "surface": "android", @@ -1427,7 +1427,7 @@ }, { "kind": "ui-call", - "line": 8123, + "line": 8027, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${hours}h", "surface": "android", @@ -1435,7 +1435,7 @@ }, { "kind": "ui-call", - "line": 8124, + "line": 8028, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${minutes}m", "surface": "android", @@ -1443,7 +1443,7 @@ }, { "kind": "ui-call", - "line": 8125, + "line": 8029, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Repeating", "surface": "android", @@ -1451,7 +1451,7 @@ }, { "kind": "ui-call", - "line": 8141, + "line": 8045, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "No prompt", "surface": "android", @@ -1459,7 +1459,7 @@ }, { "kind": "ui-call", - "line": 8158, + "line": 8062, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway", "surface": "android", @@ -1467,7 +1467,7 @@ }, { "kind": "ui-call", - "line": 8166, + "line": 8070, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connected to $gatewayLabel", "surface": "android", @@ -1475,7 +1475,7 @@ }, { "kind": "ui-call", - "line": 8167, + "line": 8071, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Your agents are ready", "surface": "android", @@ -1483,7 +1483,7 @@ }, { "kind": "ui-call", - "line": 8169, + "line": 8073, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This phone stays dormant until the gateway needs it, then wakes, syncs, and goes back to sleep.", "surface": "android", @@ -1491,7 +1491,7 @@ }, { "kind": "ui-call", - "line": 8173, + "line": 8077, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Selected on this phone", "surface": "android", @@ -1499,7 +1499,7 @@ }, { "kind": "ui-call", - "line": 8176, + "line": 8080, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "The overview refreshes on reconnect and when this screen opens.", "surface": "android", @@ -1507,7 +1507,7 @@ }, { "kind": "ui-call", - "line": 8181, + "line": 8085, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Reconnecting", "surface": "android", @@ -1515,7 +1515,7 @@ }, { "kind": "ui-call", - "line": 8182, + "line": 8086, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "OpenClaw is syncing back up", "surface": "android", @@ -1523,7 +1523,7 @@ }, { "kind": "ui-call", - "line": 8184, + "line": 8088, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "The gateway session is coming back online. Agent shortcuts should settle automatically in a moment.", "surface": "android", @@ -1531,7 +1531,7 @@ }, { "kind": "ui-call", - "line": 8188, + "line": 8092, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway session in progress", "surface": "android", @@ -1539,7 +1539,7 @@ }, { "kind": "ui-call", - "line": 8191, + "line": 8095, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "If the gateway is reachable, reconnect should complete without intervention.", "surface": "android", @@ -1547,7 +1547,7 @@ }, { "kind": "ui-call", - "line": 8196, + "line": 8100, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Welcome to OpenClaw", "surface": "android", @@ -1555,7 +1555,7 @@ }, { "kind": "ui-call", - "line": 8197, + "line": 8101, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Your phone stays quiet until it is needed", "surface": "android", @@ -1563,7 +1563,7 @@ }, { "kind": "ui-call", - "line": 8199, + "line": 8103, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Pair this device to your gateway to wake it only for real work, keep a live agent overview handy, and avoid battery-draining background loops.", "surface": "android", @@ -1571,7 +1571,7 @@ }, { "kind": "ui-call", - "line": 8203, + "line": 8107, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect to load your agents", "surface": "android", @@ -1579,7 +1579,7 @@ }, { "kind": "ui-call", - "line": 8206, + "line": 8110, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "When connected, the gateway can wake the phone with a silent push instead of holding an always-on session.", "surface": "android", @@ -1587,7 +1587,7 @@ }, { "kind": "ui-call", - "line": 8238, + "line": 8142, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Main", "surface": "android", @@ -1595,7 +1595,7 @@ }, { "kind": "ui-call", - "line": 8253, + "line": 8157, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Active on this phone", "surface": "android", @@ -1603,7 +1603,7 @@ }, { "kind": "ui-call", - "line": 8254, + "line": 8158, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Default agent", "surface": "android", @@ -1611,7 +1611,7 @@ }, { "kind": "ui-call", - "line": 8255, + "line": 8159, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Ready", "surface": "android", @@ -1619,7 +1619,7 @@ }, { "kind": "ui-call", - "line": 8910, + "line": 8814, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Dream", "surface": "android", @@ -1795,7 +1795,7 @@ }, { "kind": "ui-call", - "line": 1806, + "line": 1802, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Wait for the current response to finish before starting a new chat.", "surface": "android", @@ -1803,7 +1803,7 @@ }, { "kind": "ui-call", - "line": 1939, + "line": 1935, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not update model.", "surface": "android", @@ -1811,7 +1811,7 @@ }, { "kind": "ui-call", - "line": 2004, + "line": 2000, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not update thinking level.", "surface": "android", @@ -1819,7 +1819,7 @@ }, { "kind": "ui-call", - "line": 2523, + "line": 2513, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Chat failed before the run started; try again.", "surface": "android", @@ -1827,7 +1827,7 @@ }, { "kind": "ui-call", - "line": 4221, + "line": 4209, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not stage an attachment for sending.", "surface": "android", @@ -1835,7 +1835,7 @@ }, { "kind": "ui-call", - "line": 4254, + "line": 4242, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Offline queue is full ($OUTBOX_MAX_QUEUED messages); delete queued items first.", "surface": "android", @@ -1843,7 +1843,7 @@ }, { "kind": "ui-call", - "line": 4260, + "line": 4248, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Attachments are too large to queue for one message; remove some and try again.", "surface": "android", @@ -1851,7 +1851,7 @@ }, { "kind": "ui-call", - "line": 4266, + "line": 4254, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Offline attachment storage is full; delete queued items first.", "surface": "android", @@ -1859,7 +1859,7 @@ }, { "kind": "ui-call", - "line": 4271, + "line": 4259, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Gateway health not OK; cannot send", "surface": "android", @@ -1867,7 +1867,7 @@ }, { "kind": "ui-call", - "line": 4278, + "line": 4266, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not queue message for later delivery.", "surface": "android", @@ -1875,7 +1875,7 @@ }, { "kind": "ui-call", - "line": 5181, + "line": 5167, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Chat failed", "surface": "android", @@ -1883,7 +1883,7 @@ }, { "kind": "ui-call", - "line": 5414, + "line": 5398, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Event stream interrupted; try refreshing.", "surface": "android", @@ -1891,7 +1891,7 @@ }, { "kind": "ui-call", - "line": 5551, + "line": 5539, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Timed out waiting for a reply; try again or refresh.", "surface": "android", @@ -1899,7 +1899,7 @@ }, { "kind": "ui-call", - "line": 5761, + "line": 5747, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Timed out confirming the sent message; refresh to check delivery.", "surface": "android", @@ -14091,15 +14091,7 @@ }, { "kind": "ui-call", - "line": 3212, - "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", - "source": "Listening (PTT)", - "surface": "android", - "id": "native.android.0faf9df3e5f708df" - }, - { - "kind": "ui-call", - "line": 3223, + "line": 3202, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Microphone permission required", "surface": "android", @@ -14107,7 +14099,7 @@ }, { "kind": "ui-call", - "line": 3229, + "line": 3208, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Audio error", "surface": "android", @@ -14115,7 +14107,7 @@ }, { "kind": "ui-call", - "line": 3230, + "line": 3209, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Client error", "surface": "android", @@ -14123,7 +14115,7 @@ }, { "kind": "ui-call", - "line": 3231, + "line": 3210, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Network error", "surface": "android", @@ -14131,7 +14123,7 @@ }, { "kind": "ui-call", - "line": 3232, + "line": 3211, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Network timeout", "surface": "android", @@ -14139,23 +14131,7 @@ }, { "kind": "ui-call", - "line": 3234, - "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", - "source": "Recognizer busy", - "surface": "android", - "id": "native.android.a445b7696f9b431c" - }, - { - "kind": "ui-call", - "line": 3235, - "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", - "source": "Server error", - "surface": "android", - "id": "native.android.297438db6f5fe8d7" - }, - { - "kind": "ui-call", - "line": 3236, + "line": 3214, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Listening", "surface": "android", @@ -14163,7 +14139,31 @@ }, { "kind": "ui-call", - "line": 3237, + "line": 3214, + "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", + "source": "Listening (PTT)", + "surface": "android", + "id": "native.android.0faf9df3e5f708df" + }, + { + "kind": "ui-call", + "line": 3215, + "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", + "source": "Recognizer busy", + "surface": "android", + "id": "native.android.a445b7696f9b431c" + }, + { + "kind": "ui-call", + "line": 3216, + "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", + "source": "Server error", + "surface": "android", + "id": "native.android.297438db6f5fe8d7" + }, + { + "kind": "ui-call", + "line": 3217, "path": "apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt", "source": "Speech error ($error)", "surface": "android", @@ -25795,7 +25795,7 @@ }, { "kind": "ui-localized-call", - "line": 4238, + "line": 4165, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval needed", "surface": "apple", @@ -25803,7 +25803,7 @@ }, { "kind": "ui-localized-call", - "line": 4239, + "line": 4166, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Action required", "surface": "apple", @@ -25811,7 +25811,7 @@ }, { "kind": "ui-localized-call", - "line": 4954, + "line": 4881, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connecting...", "surface": "apple", @@ -25819,7 +25819,7 @@ }, { "kind": "ui-localized-call", - "line": 4955, + "line": 4882, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Reconnecting...", "surface": "apple", @@ -25827,7 +25827,7 @@ }, { "kind": "conditional-branch", - "line": 5310, + "line": 5237, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connected", "surface": "apple", @@ -25835,7 +25835,7 @@ }, { "kind": "conditional-branch", - "line": 5310, + "line": 5237, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Offline", "surface": "apple", @@ -25843,7 +25843,7 @@ }, { "kind": "conditional-branch", - "line": 6463, + "line": 6404, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "No chat messages yet", "surface": "apple", @@ -25851,7 +25851,7 @@ }, { "kind": "conditional-branch", - "line": 6525, + "line": 6466, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Connecting…", "surface": "apple", @@ -25859,7 +25859,7 @@ }, { "kind": "conditional-branch", - "line": 6525, + "line": 6466, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Reconnecting…", "surface": "apple", @@ -25867,7 +25867,7 @@ }, { "kind": "conditional-branch", - "line": 8872, + "line": 8813, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval", "surface": "apple", @@ -25875,7 +25875,7 @@ }, { "kind": "conditional-branch", - "line": 8872, + "line": 8813, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "This approval was already", "surface": "apple", @@ -25883,7 +25883,7 @@ }, { "kind": "conditional-branch", - "line": 8878, + "line": 8819, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "This approval was already set to Always Allow.", "surface": "apple", @@ -25891,7 +25891,7 @@ }, { "kind": "conditional-branch", - "line": 8879, + "line": 8820, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "Approval set to Always Allow.", "surface": "apple", @@ -25899,7 +25899,7 @@ }, { "kind": "conditional-branch", - "line": 10214, + "line": 10155, "path": "apps/ios/Sources/Model/NodeAppModel.swift", "source": "\\(urlText.prefix(500))…", "surface": "apple", @@ -28147,7 +28147,7 @@ }, { "kind": "ui-localized-call", - "line": 5046, + "line": 5043, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Realtime unavailable", "surface": "apple", @@ -28155,7 +28155,7 @@ }, { "kind": "ui-localized-call", - "line": 5068, + "line": 5065, "path": "apps/ios/Sources/Voice/TalkModeManager.swift", "source": "Listening (PTT)", "surface": "apple", diff --git a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt index 663d7b9243d8..1d018a982492 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt @@ -1671,7 +1671,7 @@ class NodeRuntime private constructor( nodeStatusText = "Connected" } notificationOutbox.onConnected() - showLocalCanvasOnConnect() + resetLocalCanvas() publishNodePresenceAliveBeacon(NodePresenceAliveBeacon.Trigger.Connect) val endpoint = connectedEndpoint val auth = activeGatewayAuth @@ -1692,7 +1692,7 @@ class NodeRuntime private constructor( nodeStatusText = message nodeConnectionProblem = gatewayProblemAfterDisconnect(nodeConnectionProblem, message) } - showLocalCanvasOnDisconnect() + resetLocalCanvas() }, onConnectFailure = { error, pauseReconnect -> updateStatus { @@ -2201,18 +2201,15 @@ class NodeRuntime private constructor( return if (trimmed.isEmpty()) "main" else trimmed } - private fun showLocalCanvasOnConnect() { + private fun resetLocalCanvas() { _canvasA2uiHydrated.value = false _canvasRehydratePending.value = false _canvasRehydrateErrorText.value = null canvas.navigate("") } - private fun showLocalCanvasOnDisconnect() { - _canvasA2uiHydrated.value = false - _canvasRehydratePending.value = false - _canvasRehydrateErrorText.value = null - canvas.navigate("") + private fun launchGatewayRefresh(refresh: suspend () -> Unit) { + if (mode != NodeRuntimeMode.ScreenshotFixture) scope.launch { refresh() } } fun refreshHomeCanvasOverviewIfConnected() { @@ -2237,38 +2234,15 @@ class NodeRuntime private constructor( } } - fun refreshModelCatalog() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshModelCatalogFromGateway() - } - } + fun refreshModelCatalog() = launchGatewayRefresh { refreshModelCatalogFromGateway() } - fun refreshProviderModels() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshProviderModelsFromGateway() - } - } + fun refreshProviderModels() = launchGatewayRefresh { refreshProviderModelsFromGateway() } - fun refreshTalkSetupReadiness() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { refreshTalkSetupReadinessFromGateway() } - } + fun refreshTalkSetupReadiness() = launchGatewayRefresh { refreshTalkSetupReadinessFromGateway() } - fun refreshAgents() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshAgentsFromGateway() - } - } + fun refreshAgents() = launchGatewayRefresh { refreshAgentsFromGateway() } - fun refreshCronJobs() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshCronFromGateway() - } - } + fun refreshCronJobs() = launchGatewayRefresh { refreshCronFromGateway() } fun loadCronJobDetail(id: String) { val detailRequest = cronJobDetailRequestGuard.begin(id) ?: return @@ -2441,19 +2415,9 @@ class NodeRuntime private constructor( } } - fun refreshUsage() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshUsageFromGateway() - } - } + fun refreshUsage() = launchGatewayRefresh { refreshUsageFromGateway() } - fun refreshSkills() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshSkillsFromGateway() - } - } + fun refreshSkills() = launchGatewayRefresh { refreshSkillsFromGateway() } fun setSkillEnabled( skillKey: String, @@ -2576,12 +2540,7 @@ class NodeRuntime private constructor( _skillWorkshopNoticeText.value = null } - fun refreshNodesDevices() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshNodesDevicesFromGateway() - } - } + fun refreshNodesDevices() = launchGatewayRefresh { refreshNodesDevicesFromGateway() } fun approveDevicePairing( requestId: String, @@ -2628,12 +2587,7 @@ class NodeRuntime private constructor( } } - fun refreshExecApprovals() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshExecApprovalsFromGateway() - } - } + fun refreshExecApprovals() = launchGatewayRefresh { refreshExecApprovalsFromGateway() } fun resolveExecApproval( id: String, @@ -2654,26 +2608,11 @@ class NodeRuntime private constructor( _execApprovalsNotice.compareAndSet(expected, null) } - fun refreshChannels() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshChannelsFromGateway() - } - } + fun refreshChannels() = launchGatewayRefresh { refreshChannelsFromGateway() } - fun refreshDreaming() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshDreamingFromGateway() - } - } + fun refreshDreaming() = launchGatewayRefresh { refreshDreamingFromGateway() } - fun refreshHealthLogs() { - if (mode == NodeRuntimeMode.ScreenshotFixture) return - scope.launch { - refreshHealthLogsFromGateway() - } - } + fun refreshHealthLogs() = launchGatewayRefresh { refreshHealthLogsFromGateway() } fun requestCanvasRehydrate( source: String = "manual", @@ -5475,6 +5414,36 @@ class NodeRuntime private constructor( } } + private suspend fun refreshGatewaySummary( + summary: MutableStateFlow, + refreshing: MutableStateFlow, + errorText: MutableStateFlow, + disconnectedSummary: T, + failureText: NativeText, + fetch: suspend (GatewayDataScope) -> T, + ): Boolean { + val gatewayScope = captureGatewayDataScope() ?: return false + publishGatewayData(gatewayScope) { + refreshing.value = true + errorText.value = null + } + if (!operatorConnected) { + summary.value = disconnectedSummary + refreshing.value = false + return false + } + return try { + val nextSummary = fetch(gatewayScope) + publishGatewayData(gatewayScope) { summary.value = nextSummary } + true + } catch (_: Throwable) { + publishGatewayData(gatewayScope) { errorText.value = failureText } + false + } finally { + publishGatewayData(gatewayScope) { refreshing.value = false } + } + } + /** Publishes approval state only while the response's operator socket still owns the method catalog. */ private inline fun publishGatewayApprovalData( gatewayScope: GatewayDataScope, @@ -6076,68 +6045,40 @@ class NodeRuntime private constructor( } } - private suspend fun refreshUsageFromGateway() { - val gatewayScope = captureGatewayDataScope() ?: return - publishGatewayData(gatewayScope) { - _usageRefreshing.value = true - _usageErrorText.value = null + private suspend fun refreshUsageFromGateway() = + refreshGatewaySummary( + summary = _usageSummary, + refreshing = _usageRefreshing, + errorText = _usageErrorText, + disconnectedSummary = GatewayUsageSummary(updatedAtMs = null, providers = emptyList()), + failureText = nativeText("Could not load usage."), + ) { gatewayScope -> + val root = json.parseToJsonElement(requestGatewayData(gatewayScope, "usage.status", "{}")).asObjectOrNull() + GatewayUsageSummary( + updatedAtMs = root.long("updatedAt"), + providers = parseUsageProviders(root?.get("providers") as? JsonArray), + ) } - if (!operatorConnected) { - _usageSummary.value = GatewayUsageSummary(updatedAtMs = null, providers = emptyList()) - _usageRefreshing.value = false - return - } - try { - val res = requestGatewayData(gatewayScope, "usage.status", "{}") - val root = json.parseToJsonElement(res).asObjectOrNull() - val summary = - GatewayUsageSummary( - updatedAtMs = root.long("updatedAt"), - providers = parseUsageProviders(root?.get("providers") as? JsonArray), - ) - publishGatewayData(gatewayScope) { _usageSummary.value = summary } - } catch (_: Throwable) { - publishGatewayData(gatewayScope) { _usageErrorText.value = nativeText("Could not load usage.") } - } finally { - publishGatewayData(gatewayScope) { _usageRefreshing.value = false } - } - } - private suspend fun refreshSkillsFromGateway(): Boolean { - val gatewayScope = captureGatewayDataScope() ?: return false - publishGatewayData(gatewayScope) { - _skillsRefreshing.value = true - _skillsErrorText.value = null + private suspend fun refreshSkillsFromGateway(): Boolean = + refreshGatewaySummary( + summary = _skillsSummary, + refreshing = _skillsRefreshing, + errorText = _skillsErrorText, + disconnectedSummary = GatewaySkillsSummary(skills = emptyList()), + failureText = nativeText("Could not load skills."), + ) { gatewayScope -> + val root = json.parseToJsonElement(requestGatewayData(gatewayScope, "skills.status", "{}")).asObjectOrNull() + GatewaySkillsSummary( + managedSkillsDirAvailable = + root + ?.get("managedSkillsDir") + .asStringOrNull() + ?.trim() + ?.isNotEmpty() == true, + skills = parseSkillSummaries(root?.get("skills") as? JsonArray), + ) } - if (!operatorConnected) { - _skillsSummary.value = GatewaySkillsSummary(skills = emptyList()) - _skillsRefreshing.value = false - return false - } - try { - val res = requestGatewayData(gatewayScope, "skills.status", "{}") - val root = json.parseToJsonElement(res).asObjectOrNull() - val summary = - GatewaySkillsSummary( - managedSkillsDirAvailable = - root - ?.get("managedSkillsDir") - .asStringOrNull() - ?.trim() - ?.isNotEmpty() == true, - skills = parseSkillSummaries(root?.get("skills") as? JsonArray), - ) - publishGatewayData(gatewayScope) { - _skillsSummary.value = summary - } - return true - } catch (_: Throwable) { - publishGatewayData(gatewayScope) { _skillsErrorText.value = nativeText("Could not load skills.") } - return false - } finally { - publishGatewayData(gatewayScope) { _skillsRefreshing.value = false } - } - } private suspend fun setSkillEnabledOnGateway( skillKey: String, @@ -7542,101 +7483,64 @@ class NodeRuntime private constructor( } } - private suspend fun refreshChannelsFromGateway() { - val gatewayScope = captureGatewayDataScope() ?: return - publishGatewayData(gatewayScope) { - _channelsRefreshing.value = true - _channelsErrorText.value = null + private suspend fun refreshChannelsFromGateway() = + refreshGatewaySummary( + summary = _channelsSummary, + refreshing = _channelsRefreshing, + errorText = _channelsErrorText, + disconnectedSummary = GatewayChannelsSummary(channels = emptyList()), + failureText = nativeText("Could not load channels."), + ) { gatewayScope -> + val response = requestGatewayData(gatewayScope, "channels.status", """{"probe":false,"timeoutMs":8000}""") + val root = json.parseToJsonElement(response).asObjectOrNull() + GatewayChannelsSummary( + updatedAtMs = root.long("ts"), + partial = root.boolean("partial"), + warnings = parseStringArray(root?.get("warnings") as? JsonArray), + channels = parseChannelSummaries(root), + ) } - if (!operatorConnected) { - _channelsSummary.value = GatewayChannelsSummary(channels = emptyList()) - _channelsRefreshing.value = false - return - } - try { - val res = requestGatewayData(gatewayScope, "channels.status", """{"probe":false,"timeoutMs":8000}""") - val root = json.parseToJsonElement(res).asObjectOrNull() - val summary = - GatewayChannelsSummary( - updatedAtMs = root.long("ts"), - partial = root.boolean("partial"), - warnings = parseStringArray(root?.get("warnings") as? JsonArray), - channels = parseChannelSummaries(root), - ) - publishGatewayData(gatewayScope) { _channelsSummary.value = summary } - } catch (_: Throwable) { - publishGatewayData(gatewayScope) { _channelsErrorText.value = nativeText("Could not load channels.") } - } finally { - publishGatewayData(gatewayScope) { _channelsRefreshing.value = false } - } - } - private suspend fun refreshDreamingFromGateway() { - val gatewayScope = captureGatewayDataScope() ?: return - publishGatewayData(gatewayScope) { - _dreamingRefreshing.value = true - _dreamingErrorText.value = null + private suspend fun refreshDreamingFromGateway() = + refreshGatewaySummary( + summary = _dreamingSummary, + refreshing = _dreamingRefreshing, + errorText = _dreamingErrorText, + disconnectedSummary = GatewayDreamingSummary(), + failureText = nativeText("Could not load dreaming."), + ) { gatewayScope -> + val statusResponse = requestGatewayData(gatewayScope, "doctor.memory.status", "{}") + val statusRoot = json.parseToJsonElement(statusResponse).asObjectOrNull() + val diaryResponse = requestGatewayData(gatewayScope, "doctor.memory.dreamDiary", "{}") + val diaryRoot = json.parseToJsonElement(diaryResponse).asObjectOrNull() + parseDreamingSummary(dreaming = statusRoot?.get("dreaming").asObjectOrNull(), diary = diaryRoot) } - if (!operatorConnected) { - _dreamingSummary.value = GatewayDreamingSummary() - _dreamingRefreshing.value = false - return - } - try { - val statusRes = requestGatewayData(gatewayScope, "doctor.memory.status", "{}") - val statusRoot = json.parseToJsonElement(statusRes).asObjectOrNull() - val diaryRes = requestGatewayData(gatewayScope, "doctor.memory.dreamDiary", "{}") - val diaryRoot = json.parseToJsonElement(diaryRes).asObjectOrNull() - val dreaming = statusRoot?.get("dreaming").asObjectOrNull() - val summary = - parseDreamingSummary( - dreaming = dreaming, - diary = diaryRoot, - ) - publishGatewayData(gatewayScope) { _dreamingSummary.value = summary } - } catch (_: Throwable) { - publishGatewayData(gatewayScope) { _dreamingErrorText.value = nativeText("Could not load dreaming.") } - } finally { - publishGatewayData(gatewayScope) { _dreamingRefreshing.value = false } - } - } - private suspend fun refreshHealthLogsFromGateway() { - val gatewayScope = captureGatewayDataScope() ?: return - publishGatewayData(gatewayScope) { - _healthLogsRefreshing.value = true - _healthLogsErrorText.value = null - } - if (!operatorConnected) { - _healthLogsSummary.value = GatewayHealthLogsSummary() - _healthLogsRefreshing.value = false - return - } - try { - val res = requestGatewayData(gatewayScope, "logs.tail", """{"limit":40,"maxBytes":65536}""") - val root = json.parseToJsonElement(res).asObjectOrNull() + private suspend fun refreshHealthLogsFromGateway() = + refreshGatewaySummary( + summary = _healthLogsSummary, + refreshing = _healthLogsRefreshing, + errorText = _healthLogsErrorText, + disconnectedSummary = GatewayHealthLogsSummary(), + failureText = nativeText("Could not load gateway logs."), + ) { gatewayScope -> + val response = requestGatewayData(gatewayScope, "logs.tail", """{"limit":40,"maxBytes":65536}""") + val root = json.parseToJsonElement(response).asObjectOrNull() val lines = (root?.get("lines") as? JsonArray)?.mapNotNull { it.asStringOrNull() }.orEmpty() - val summary = - GatewayHealthLogsSummary( - fileName = - root - ?.get("file") - .asStringOrNull() - ?.trim() - ?.takeIf { it.isNotEmpty() } - ?.substringAfterLast('/') - ?.substringAfterLast('\\'), - cursor = root.long("cursor"), - truncated = root.boolean("truncated"), - entries = lines.map { parseGatewayLogEntry(it) }, - ) - publishGatewayData(gatewayScope) { _healthLogsSummary.value = summary } - } catch (_: Throwable) { - publishGatewayData(gatewayScope) { _healthLogsErrorText.value = nativeText("Could not load gateway logs.") } - } finally { - publishGatewayData(gatewayScope) { _healthLogsRefreshing.value = false } + GatewayHealthLogsSummary( + fileName = + root + ?.get("file") + .asStringOrNull() + ?.trim() + ?.takeIf { it.isNotEmpty() } + ?.substringAfterLast('/') + ?.substringAfterLast('\\'), + cursor = root.long("cursor"), + truncated = root.boolean("truncated"), + entries = lines.map { parseGatewayLogEntry(it) }, + ) } - } private fun parseGatewayLogEntry(line: String): GatewayLogEntry { val sanitizedLine = sanitizeGatewayLogText(line) diff --git a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt index f704d7bfc555..705974edf704 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt @@ -508,9 +508,7 @@ class ChatController internal constructor( clearOptimisticMessages = false, preserveDisconnectedOwnership = true, ) - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() // Older gateways cannot restate plan state, so reconnect retains it until // recovery proves another run, a terminal state, or an explicit empty snapshot. _historyLoading.value = false @@ -617,9 +615,7 @@ class ChatController internal constructor( if (retireRunState) { restoreRunStateOnReconnect = false clearPendingRuns() - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() } clearPlanSteps() appliedMainSessionKey = "main" @@ -2183,9 +2179,7 @@ class ChatController internal constructor( _healthOk.value = false clearLiveHistoryMarker() clearPendingRuns() - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanSteps() _sessionId.value = null _historyLoading.value = markLoading @@ -2505,9 +2499,7 @@ class ChatController internal constructor( settleProjectedRun(actualRunId) if (ack.isTerminalSuccess) { if (isCapturedOwnerCurrent()) { - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanSteps() refreshCurrentHistoryBestEffort(runIdsToReconcile = setOf(actualRunId)) } @@ -2516,9 +2508,7 @@ class ChatController internal constructor( // Terminal timeout/error means the gateway did not accept a runnable turn. // Surface failed acceptance instead of letting a cleared composer look successful. if (isCapturedOwnerCurrent()) { - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanSteps() updateLocalizedErrorText(nativeText("Chat failed before the run started; try again.")) } @@ -2871,9 +2861,7 @@ class ChatController internal constructor( // retain local ownership until the recovery snapshot can reconcile it. resetSwarmProgress() if (isSwarmEnabled()) refreshSwarmSessions() - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() refreshQuestions() refreshHistoryForRecovery() } @@ -5154,9 +5142,7 @@ class ChatController internal constructor( val hasNewerRun = synchronized(pendingRuns) { pendingRuns.isNotEmpty() } || unresolvedRepliesByRunId.isNotEmpty() if (!hasNewerRun) { - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanStepsFor(runId) updateLocalizedErrorText( if (state == "error") { @@ -5188,9 +5174,7 @@ class ChatController internal constructor( } else { clearPendingRuns(clearOptimisticMessages = false) } - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanStepsFor(runId) val terminalRunIds = runId?.let(::setOf) ?: unresolvedRepliesByRunId.keys.toSet() refreshCurrentHistoryBestEffort( @@ -5413,9 +5397,7 @@ class ChatController internal constructor( "error" -> { updateLocalizedErrorText(nativeText("Event stream interrupted; try refreshing.")) clearPendingRuns() - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() clearPlanSteps() } } @@ -5441,6 +5423,12 @@ class ChatController internal constructor( pendingToolCallsById.values.sortedBy { it.startedAtMs } } + private fun clearLiveRunUi() { + pendingToolCallsById.clear() + publishPendingToolCalls() + _streamingAssistantText.value = null + } + private fun clearPlanSteps() { planRunId = null _planSteps.value = emptyList() @@ -5586,9 +5574,7 @@ class ChatController internal constructor( private fun clearTransientRunUiIfIdle(preservePlan: Boolean = false) { if (synchronized(pendingRuns) { pendingRuns.isNotEmpty() }) return - pendingToolCallsById.clear() - publishPendingToolCalls() - _streamingAssistantText.value = null + clearLiveRunUi() if (!preservePlan) clearPlanSteps() } diff --git a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt index da72b89d2286..b598fb634547 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/voice/TalkModeManager.kt @@ -3191,33 +3191,12 @@ class TalkModeManager internal constructor( if (stopRequested) return _isListening.value = false _inputLevel.value = 0f - if (activePttCaptureId != null) { + val pushToTalkActive = activePttCaptureId != null + if (pushToTalkActive) { pttReleaseCompletion?.let { it.complete(Unit) return } - if (error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS) { - setStatus(nativeText("Microphone permission required")) - return - } - setStatus( - when (error) { - SpeechRecognizer.ERROR_AUDIO -> nativeText("Audio error") - SpeechRecognizer.ERROR_CLIENT -> nativeText("Client error") - SpeechRecognizer.ERROR_NETWORK -> nativeText("Network error") - SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> nativeText("Network timeout") - SpeechRecognizer.ERROR_NO_MATCH -> nativeText("Listening (PTT)") - SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> nativeText("Recognizer busy") - SpeechRecognizer.ERROR_SERVER -> nativeText("Server error") - SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> nativeText("Listening (PTT)") - else -> nativeText("Speech error (\$error)", error) - }, - ) - schedulePushToTalkRestart( - delayMs = 600L, - advanceRung = pttRecognitionRung !is PushToTalkRecognitionRung.RestartingSingleSession, - ) - return } if (error == SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS) { setStatus(nativeText("Microphone permission required")) @@ -3230,13 +3209,21 @@ class TalkModeManager internal constructor( SpeechRecognizer.ERROR_CLIENT -> nativeText("Client error") SpeechRecognizer.ERROR_NETWORK -> nativeText("Network error") SpeechRecognizer.ERROR_NETWORK_TIMEOUT -> nativeText("Network timeout") - SpeechRecognizer.ERROR_NO_MATCH -> nativeText("Listening") + SpeechRecognizer.ERROR_NO_MATCH, + SpeechRecognizer.ERROR_SPEECH_TIMEOUT, + -> if (pushToTalkActive) nativeText("Listening (PTT)") else nativeText("Listening") SpeechRecognizer.ERROR_RECOGNIZER_BUSY -> nativeText("Recognizer busy") SpeechRecognizer.ERROR_SERVER -> nativeText("Server error") - SpeechRecognizer.ERROR_SPEECH_TIMEOUT -> nativeText("Listening") else -> nativeText("Speech error (\$error)", error) }, ) + if (pushToTalkActive) { + schedulePushToTalkRestart( + delayMs = 600L, + advanceRung = pttRecognitionRung !is PushToTalkRecognitionRung.RestartingSingleSession, + ) + return + } scheduleRestart(delayMs = 600) } diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index 48346fc4e2a4..b485b25046c9 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -2301,6 +2301,23 @@ final class NodeAppModel { } } + private static func successfulInvokeResponse( + _ request: BridgeInvokeRequest, + payload: some Encodable) throws -> BridgeInvokeResponse + { + try BridgeInvokeResponse( + id: request.id, + ok: true, + payloadJSON: self.encodePayload(payload)) + } + + private static func unknownInvokeResponse(_ request: BridgeInvokeRequest) -> BridgeInvokeResponse { + BridgeInvokeResponse( + id: request.id, + ok: false, + error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + } + private static func scopedWatchNotificationRequest( _ req: BridgeInvokeRequest, gatewayStableID: String?) -> BridgeInvokeRequest @@ -2379,8 +2396,7 @@ final class NodeAppModel { timestamp: ISO8601DateFormatter().string(from: location.timestamp), isPrecise: isPrecise, source: nil) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) } private func handleCanvasInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { @@ -2431,10 +2447,7 @@ final class NodeAppModel { ]) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2507,10 +2520,7 @@ final class NodeAppModel { return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: resultJSON) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2581,10 +2591,7 @@ final class NodeAppModel { updateCameraHUD(ownerID: req.id, text: "Clip captured", kind: .success, autoHideSeconds: 1.8) return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2739,8 +2746,7 @@ final class NodeAppModel { } let payload = OpenClawChatPushPayload(messageId: messageId) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) } private func notificationAuthorizationStatus() async -> NotificationAuthorizationStatus { @@ -2802,17 +2808,12 @@ final class NodeAppModel { switch req.command { case OpenClawDeviceCommand.status.rawValue: let payload = try await deviceStatusService.status() - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawDeviceCommand.info.rawValue: let payload = self.deviceStatusService.info() - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2820,8 +2821,7 @@ final class NodeAppModel { let params = (try? Self.decodeParams(OpenClawPhotosLatestParams.self, from: req.paramsJSON)) ?? OpenClawPhotosLatestParams() let payload = try await photosService.latest(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) } private func handleContactsInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { @@ -2830,18 +2830,13 @@ final class NodeAppModel { let params = (try? Self.decodeParams(OpenClawContactsSearchParams.self, from: req.paramsJSON)) ?? OpenClawContactsSearchParams() let payload = try await contactsService.search(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawContactsCommand.add.rawValue: let params = try Self.decodeParams(OpenClawContactsAddParams.self, from: req.paramsJSON) let payload = try await contactsService.add(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2851,18 +2846,13 @@ final class NodeAppModel { let params = (try? Self.decodeParams(OpenClawCalendarEventsParams.self, from: req.paramsJSON)) ?? OpenClawCalendarEventsParams() let payload = try await calendarService.events(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawCalendarCommand.add.rawValue: let params = try Self.decodeParams(OpenClawCalendarAddParams.self, from: req.paramsJSON) let payload = try await calendarService.add(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2872,18 +2862,13 @@ final class NodeAppModel { let params = (try? Self.decodeParams(OpenClawRemindersListParams.self, from: req.paramsJSON)) ?? OpenClawRemindersListParams() let payload = try await remindersService.list(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawRemindersCommand.add.rawValue: let params = try Self.decodeParams(OpenClawRemindersAddParams.self, from: req.paramsJSON) let payload = try await remindersService.add(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2893,19 +2878,14 @@ final class NodeAppModel { let params = (try? Self.decodeParams(OpenClawMotionActivityParams.self, from: req.paramsJSON)) ?? OpenClawMotionActivityParams() let payload = try await motionService.activities(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawMotionCommand.pedometer.rawValue: let params = (try? Self.decodeParams(OpenClawPedometerParams.self, from: req.paramsJSON)) ?? OpenClawPedometerParams() let payload = try await motionService.pedometer(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -2919,8 +2899,7 @@ final class NodeAppModel { message: "INVALID_REQUEST: period must be today")) } let payload = try await self.healthSummaryService.summary(params: params) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) } private func handleTalkInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { @@ -2988,25 +2967,19 @@ final class NodeAppModel { case .started: await self.talkMode.awaitPushToTalkOnce(start) } - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawTalkCommand.pttStop.rawValue: // Interrupt commands invalidate suspended preparation before touching // capture state, then bypass the preparation queue entirely. self.talkPttCommandEpoch &+= 1 let payload = self.talkMode.endPushToTalk(expectedTranscriptionOnly: false) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawTalkCommand.pttCancel.rawValue: self.talkPttCommandEpoch &+= 1 let payload = self.talkMode.cancelPushToTalk(expectedTranscriptionOnly: false) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -3283,16 +3256,20 @@ extension NodeAppModel { private func buildCapabilityRouter() -> NodeCapabilityRouter { var handlers: [String: NodeCapabilityRouter.Handler] = [:] - func register(_ commands: [String], handler: @escaping NodeCapabilityRouter.Handler) { + func register( + _ commands: [String], + handler: @escaping (NodeAppModel, BridgeInvokeRequest) async throws -> BridgeInvokeResponse) + { + let invoke: NodeCapabilityRouter.Handler = { [weak self] request in + guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } + return try await handler(self, request) + } for command in commands { - handlers[command] = handler + handlers[command] = invoke } } - register([OpenClawLocationCommand.get.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleLocationInvoke(req) - } + register([OpenClawLocationCommand.get.rawValue]) { try await $0.handleLocationInvoke($1) } register([ OpenClawCanvasCommand.present.rawValue, @@ -3300,111 +3277,66 @@ extension NodeAppModel { OpenClawCanvasCommand.navigate.rawValue, OpenClawCanvasCommand.evalJS.rawValue, OpenClawCanvasCommand.snapshot.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleCanvasInvoke(req) - } + ]) { try await $0.handleCanvasInvoke($1) } register([ OpenClawCanvasA2UICommand.reset.rawValue, OpenClawCanvasA2UICommand.push.rawValue, OpenClawCanvasA2UICommand.pushJSONL.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleCanvasA2UIInvoke(req) - } + ]) { try await $0.handleCanvasA2UIInvoke($1) } register([ OpenClawCameraCommand.list.rawValue, OpenClawCameraCommand.snap.rawValue, OpenClawCameraCommand.clip.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleCameraInvoke(req) - } + ]) { try await $0.handleCameraInvoke($1) } - register([OpenClawScreenCommand.record.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleScreenRecordInvoke(req) - } + register([OpenClawScreenCommand.record.rawValue]) { try await $0.handleScreenRecordInvoke($1) } - register([OpenClawSystemCommand.notify.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleSystemNotify(req) - } + register([OpenClawSystemCommand.notify.rawValue]) { try await $0.handleSystemNotify($1) } - register([OpenClawChatCommand.push.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleChatPushInvoke(req) - } + register([OpenClawChatCommand.push.rawValue]) { try await $0.handleChatPushInvoke($1) } register([ OpenClawDeviceCommand.status.rawValue, OpenClawDeviceCommand.info.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleDeviceInvoke(req) - } + ]) { try await $0.handleDeviceInvoke($1) } register([ OpenClawWatchCommand.status.rawValue, OpenClawWatchCommand.notify.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleWatchInvoke(req) - } + ]) { try await $0.handleWatchInvoke($1) } - register([OpenClawPhotosCommand.latest.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handlePhotosInvoke(req) - } + register([OpenClawPhotosCommand.latest.rawValue]) { try await $0.handlePhotosInvoke($1) } register([ OpenClawContactsCommand.search.rawValue, OpenClawContactsCommand.add.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleContactsInvoke(req) - } + ]) { try await $0.handleContactsInvoke($1) } register([ OpenClawCalendarCommand.events.rawValue, OpenClawCalendarCommand.add.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleCalendarInvoke(req) - } + ]) { try await $0.handleCalendarInvoke($1) } register([ OpenClawRemindersCommand.list.rawValue, OpenClawRemindersCommand.add.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleRemindersInvoke(req) - } + ]) { try await $0.handleRemindersInvoke($1) } register([ OpenClawMotionCommand.activity.rawValue, OpenClawMotionCommand.pedometer.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleMotionInvoke(req) - } + ]) { try await $0.handleMotionInvoke($1) } - register([OpenClawHealthCommand.summary.rawValue]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleHealthInvoke(req) - } + register([OpenClawHealthCommand.summary.rawValue]) { try await $0.handleHealthInvoke($1) } register([ OpenClawTalkCommand.pttStart.rawValue, OpenClawTalkCommand.pttStop.rawValue, OpenClawTalkCommand.pttCancel.rawValue, OpenClawTalkCommand.pttOnce.rawValue, - ]) { [weak self] req in - guard let self else { throw NodeCapabilityRouter.RouterError.handlerUnavailable } - return try await self.handleTalkInvoke(req) - } + ]) { try await $0.handleTalkInvoke($1) } return NodeCapabilityRouter(handlers: handlers) } @@ -3419,8 +3351,7 @@ extension NodeAppModel { appInstalled: status.appInstalled, reachable: status.reachable, activationState: status.activationState) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) case OpenClawWatchCommand.notify.rawValue: let params = try Self.decodeParams(OpenClawWatchNotifyParams.self, from: req.paramsJSON) let normalizedParams = Self.normalizeWatchNotifyParams(params) @@ -3457,8 +3388,7 @@ extension NodeAppModel { deliveredImmediately: result.deliveredImmediately, queuedForDelivery: result.queuedForDelivery, transport: result.transport) - let json = try Self.encodePayload(payload) - return BridgeInvokeResponse(id: req.id, ok: true, payloadJSON: json) + return try Self.successfulInvokeResponse(req, payload: payload) } catch { return BridgeInvokeResponse( id: req.id, @@ -3468,10 +3398,7 @@ extension NodeAppModel { message: error.localizedDescription)) } default: - return BridgeInvokeResponse( - id: req.id, - ok: false, - error: OpenClawNodeError(code: .invalidRequest, message: "INVALID_REQUEST: unknown command")) + return Self.unknownInvokeResponse(req) } } @@ -6047,14 +5974,38 @@ extension NodeAppModel { await self.syncWatchExecApprovalSnapshot(reason: reason) } + private static func appendPendingApproval( + _ item: Item, + to items: inout [Item], + maximumCount: Int? = nil, + key: (Item) -> (some Equatable)?, + sortedBy sortsBefore: (Item, Item) -> Bool) -> Bool + { + guard let itemKey = key(item), !items.contains(where: { key($0) == itemKey }) else { return false } + items.append(item) + if let maximumCount, items.count > maximumCount { items.removeFirst() } + items.sort(by: sortsBefore) + return true + } + + private static func removePendingApproval( + _ item: Item, + from items: inout [Item], + key: (Item) -> (some Equatable)?) -> Bool + { + guard let itemKey = key(item) else { return false } + let originalCount = items.count + items.removeAll { key($0) == itemKey } + return items.count != originalCount + } + private func appendPendingWatchExecApprovalRecoveryPush(_ push: ExecApprovalNotificationPrompt) { - guard let pushKey = Self.execApprovalPushKey(push), - !self.pendingWatchExecApprovalRecoveryPushes.contains(where: { - Self.execApprovalPushKey($0) == pushKey - }) + guard Self.appendPendingApproval( + push, + to: &self.pendingWatchExecApprovalRecoveryPushes, + key: Self.execApprovalPushKey, + sortedBy: Self.execApprovalPushSortsBefore) else { return } - self.pendingWatchExecApprovalRecoveryPushes.append(push) - self.pendingWatchExecApprovalRecoveryPushes.sort(by: Self.execApprovalPushSortsBefore) GatewayDiagnostics.log( "watch exec approval: queued recovery " + "id=\(push.approvalId) pendingCount=\(self.pendingWatchExecApprovalRecoveryPushes.count)") @@ -6062,12 +6013,11 @@ extension NodeAppModel { } private func removePendingWatchExecApprovalRecoveryPush(_ push: ExecApprovalNotificationPrompt) { - guard let pushKey = Self.execApprovalPushKey(push) else { return } - let originalCount = self.pendingWatchExecApprovalRecoveryPushes.count - self.pendingWatchExecApprovalRecoveryPushes.removeAll { - Self.execApprovalPushKey($0) == pushKey - } - guard self.pendingWatchExecApprovalRecoveryPushes.count != originalCount else { return } + guard Self.removePendingApproval( + push, + from: &self.pendingWatchExecApprovalRecoveryPushes, + key: Self.execApprovalPushKey) + else { return } GatewayDiagnostics.log( "watch exec approval: cleared recovery " + "id=\(push.approvalId) pendingCount=\(self.pendingWatchExecApprovalRecoveryPushes.count)") @@ -6075,40 +6025,35 @@ extension NodeAppModel { } private func appendPendingExecApprovalResolvedPush(_ push: ExecApprovalNotificationPrompt) { - guard let pushKey = Self.execApprovalPushKey(push), - !self.pendingExecApprovalResolvedPushes.contains(where: { - Self.execApprovalPushKey($0) == pushKey - }) - else { return } // A silent resolution push is not replayed by the gateway. Keep it until the // authenticated owner route returns so its matching notification cannot linger. - self.pendingExecApprovalResolvedPushes.append(push) - if self.pendingExecApprovalResolvedPushes.count > 32 { - self.pendingExecApprovalResolvedPushes.removeFirst() - } - self.pendingExecApprovalResolvedPushes.sort(by: Self.execApprovalPushSortsBefore) + guard Self.appendPendingApproval( + push, + to: &self.pendingExecApprovalResolvedPushes, + maximumCount: 32, + key: Self.execApprovalPushKey, + sortedBy: Self.execApprovalPushSortsBefore) + else { return } self.persistWatchExecApprovalBridgeState() } private func removePendingExecApprovalResolvedPush(_ push: ExecApprovalNotificationPrompt) { - guard let pushKey = Self.execApprovalPushKey(push) else { return } - let originalCount = self.pendingExecApprovalResolvedPushes.count - self.pendingExecApprovalResolvedPushes.removeAll { - Self.execApprovalPushKey($0) == pushKey - } - guard self.pendingExecApprovalResolvedPushes.count != originalCount else { return } + guard Self.removePendingApproval( + push, + from: &self.pendingExecApprovalResolvedPushes, + key: Self.execApprovalPushKey) + else { return } self.persistWatchExecApprovalBridgeState() } private func removePendingPersistedExecApprovalReadback( _ readback: PersistedExecApprovalReadback) { - guard let readbackKey = Self.persistedExecApprovalReadbackKey(readback) else { return } - let originalCount = self.pendingPersistedExecApprovalReadbacks.count - self.pendingPersistedExecApprovalReadbacks.removeAll { - Self.persistedExecApprovalReadbackKey($0) == readbackKey - } - guard self.pendingPersistedExecApprovalReadbacks.count != originalCount else { return } + guard Self.removePendingApproval( + readback, + from: &self.pendingPersistedExecApprovalReadbacks, + key: Self.persistedExecApprovalReadbackKey) + else { return } self.persistWatchExecApprovalBridgeState() } @@ -6119,19 +6064,15 @@ extension NodeAppModel { let readback = PersistedExecApprovalReadback( approvalId: approvalId, gatewayStableID: gatewayStableID) - guard let readbackKey = Self.persistedExecApprovalReadbackKey(readback), - !self.pendingPersistedExecApprovalReadbacks.contains(where: { - Self.persistedExecApprovalReadbackKey($0) == readbackKey - }) - else { return } // A requested event is an edge trigger, not replayed state. Retain its exact owner // until approval.get classifies it so a reconnect cannot lose a parked approval. - self.pendingPersistedExecApprovalReadbacks.append(readback) - if self.pendingPersistedExecApprovalReadbacks.count > 64 { - self.pendingPersistedExecApprovalReadbacks.removeFirst() - } - self.pendingPersistedExecApprovalReadbacks.sort( - by: Self.persistedExecApprovalReadbackSortsBefore) + guard Self.appendPendingApproval( + readback, + to: &self.pendingPersistedExecApprovalReadbacks, + maximumCount: 64, + key: Self.persistedExecApprovalReadbackKey, + sortedBy: Self.persistedExecApprovalReadbackSortsBefore) + else { return } self.persistWatchExecApprovalBridgeState() } diff --git a/apps/ios/Sources/Voice/TalkModeManager.swift b/apps/ios/Sources/Voice/TalkModeManager.swift index c836882bb7f1..5370f542bf2a 100644 --- a/apps/ios/Sources/Voice/TalkModeManager.swift +++ b/apps/ios/Sources/Voice/TalkModeManager.swift @@ -4577,34 +4577,31 @@ extension TalkModeManager { } static func configureAudioSession(preferredInputDeviceID: String? = nil) throws { - let session = AVAudioSession.sharedInstance() - let forceSpeaker = TalkDefaults.speakerphoneEnabled() - let options = TalkAudioRoute.categoryOptions(speakerphoneEnabled: forceSpeaker) // Prefer `.spokenAudio` for STT; it tends to preserve speech energy better than `.voiceChat`. - try session.setCategory(.playAndRecord, mode: .spokenAudio, options: options) - try? session.setPreferredSampleRate(48000) - try? session.setPreferredIOBufferDuration(0.02) - try session.setActive(true, options: []) - if TalkAudioRoute.shouldForceSpeaker( - preferenceEnabled: forceSpeaker, - outputPortTypes: session.currentRoute.outputs.map(\.portType)) - { - try? session.overrideOutputAudioPort(.speaker) - } else { - try? session.overrideOutputAudioPort(.none) - } - // A speaker override also forces the built-in microphone; apply an explicit input last. - self.applyPreferredInput(preferredInputDeviceID, to: session) - GatewayDiagnostics.log("talk audio: session speakerphone=\(forceSpeaker) \(Self.describeAudioSession())") + try self.configureAudioSession( + mode: .spokenAudio, + preferredInputDeviceID: preferredInputDeviceID, + diagnosticsPrefix: "talk audio") } static func configureRealtimeAudioSession(preferredInputDeviceID: String? = nil) throws { + // Realtime Talk is full duplex. `.voiceChat` enables iOS voice processing so speaker + // output is less likely to be captured as fresh microphone input. + try self.configureAudioSession( + mode: .voiceChat, + preferredInputDeviceID: preferredInputDeviceID, + diagnosticsPrefix: "talk realtime audio") + } + + private static func configureAudioSession( + mode: AVAudioSession.Mode, + preferredInputDeviceID: String?, + diagnosticsPrefix: String) throws + { let session = AVAudioSession.sharedInstance() let forceSpeaker = TalkDefaults.speakerphoneEnabled() let options = TalkAudioRoute.categoryOptions(speakerphoneEnabled: forceSpeaker) - // Realtime Talk is full duplex. `.voiceChat` enables iOS voice processing so speaker - // output is less likely to be captured as fresh microphone input. - try session.setCategory(.playAndRecord, mode: .voiceChat, options: options) + try session.setCategory(.playAndRecord, mode: mode, options: options) try? session.setPreferredSampleRate(48000) try? session.setPreferredIOBufferDuration(0.02) try session.setActive(true, options: []) @@ -4619,7 +4616,7 @@ extension TalkModeManager { // A speaker override also forces the built-in microphone; apply an explicit input last. self.applyPreferredInput(preferredInputDeviceID, to: session) GatewayDiagnostics.log( - "talk realtime audio: session speakerphone=\(forceSpeaker) \(Self.describeAudioSession())") + "\(diagnosticsPrefix): session speakerphone=\(forceSpeaker) \(Self.describeAudioSession())") } private static func applyPreferredInput(_ deviceID: String?, to session: AVAudioSession) { diff --git a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift index be0dcffa5f8d..639855f342df 100644 --- a/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift +++ b/apps/shared/OpenClawKit/Tests/OpenClawKitTests/ChatViewModelTests.swift @@ -1943,7 +1943,9 @@ struct ChatViewModelTests { let discardedViewModel = try weakReference(to: viewModel) viewModel = nil - await Task.yield() + for _ in 0..<100 where discardedViewModel.value != nil { + await Task.yield() + } #expect(discardedViewModel.value == nil) } diff --git a/test/vitest/vitest.extension-codex-app-server-attempt-extra.config.ts b/test/vitest/vitest.extension-codex-app-server-attempt-extra.config.ts index d827931767ec..dd96461c60f6 100644 --- a/test/vitest/vitest.extension-codex-app-server-attempt-extra.config.ts +++ b/test/vitest/vitest.extension-codex-app-server-attempt-extra.config.ts @@ -6,6 +6,7 @@ function createExtensionCodexAppServerAttemptExtraVitestConfig( ) { return createScopedVitestConfig( [ + "extensions/codex/src/app-server/run-attempt-client-prewarm.test.ts", "extensions/codex/src/app-server/run-attempt-lifecycle-controller.test.ts", "extensions/codex/src/app-server/run-attempt-thread-cleanup.test.ts", "extensions/codex/src/app-server/run-attempt.context-engine.test.ts",