From 8c3c8a2ab08b1da74decfe9dfb6ab2e303a5acf7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 10 Aug 2026 21:08:44 -0700 Subject: [PATCH] feat(android): show live subagent activity in chat (#121813) * feat(android): show live subagent activity * chore(android): mark task event handled * fix(android): localize activity diff chips * fix(android): reconcile dropped task activity --- apps/.i18n/native-source.json | 420 ++++++++++-------- .../java/ai/openclaw/app/MainViewModel.kt | 2 + .../main/java/ai/openclaw/app/NodeRuntime.kt | 1 + .../ai/openclaw/app/chat/BackgroundTask.kt | 8 +- .../ai/openclaw/app/chat/ChatController.kt | 165 ++++++- .../java/ai/openclaw/app/chat/ChatModels.kt | 22 + .../ai/openclaw/app/ui/chat/ChatScreen.kt | 142 +++++- .../ai/openclaw/app/ui/chat/ChatTimeline.kt | 59 +++ .../openclaw/app/chat/BackgroundTaskTest.kt | 11 + .../chat/ChatControllerStreamReplayTest.kt | 43 ++ .../ChatControllerSubagentActivityTest.kt | 163 +++++++ .../openclaw/app/ui/chat/ChatTimelineTest.kt | 94 ++++ .../protocol-event-coverage.allowlist.json | 1 - 13 files changed, 929 insertions(+), 202 deletions(-) create mode 100644 apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerSubagentActivityTest.kt diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index 8a33f665993b..09ac49b0a53e 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -1051,7 +1051,7 @@ }, { "kind": "ui-call", - "line": 3875, + "line": 3876, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect to a Gateway to save wake words", "surface": "android", @@ -1059,7 +1059,7 @@ }, { "kind": "ui-call", - "line": 3903, + "line": 3904, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Wake words saved", "surface": "android", @@ -1067,7 +1067,7 @@ }, { "kind": "ui-call", - "line": 3912, + "line": 3913, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not save wake words", "surface": "android", @@ -1075,7 +1075,7 @@ }, { "kind": "ui-call", - "line": 4625, + "line": 4626, "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", @@ -1083,7 +1083,7 @@ }, { "kind": "ui-call", - "line": 4629, + "line": 4630, "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", @@ -1091,7 +1091,7 @@ }, { "kind": "ui-call", - "line": 4633, + "line": 4634, "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", @@ -1099,7 +1099,7 @@ }, { "kind": "ui-named-argument", - "line": 4683, + "line": 4684, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connecting…", "surface": "android", @@ -1107,7 +1107,7 @@ }, { "kind": "ui-named-argument", - "line": 4887, + "line": 4888, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Offline", "surface": "android", @@ -1115,7 +1115,7 @@ }, { "kind": "ui-call", - "line": 5741, + "line": 5742, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load provider catalog.", "surface": "android", @@ -1123,7 +1123,7 @@ }, { "kind": "ui-call", - "line": 5772, + "line": 5773, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Update your Gateway to view provider model config.", "surface": "android", @@ -1131,7 +1131,7 @@ }, { "kind": "ui-call", - "line": 5774, + "line": 5775, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load provider model config.", "surface": "android", @@ -1139,7 +1139,7 @@ }, { "kind": "ui-call", - "line": 5790, + "line": 5791, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Provider models loaded, but readiness is unavailable.", "surface": "android", @@ -1147,7 +1147,7 @@ }, { "kind": "ui-call", - "line": 5877, + "line": 5878, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automations.", "surface": "android", @@ -1155,7 +1155,7 @@ }, { "kind": "ui-call", - "line": 5963, + "line": 5964, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect automations.", "surface": "android", @@ -1163,7 +1163,7 @@ }, { "kind": "ui-call", - "line": 5973, + "line": 5974, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway returned an invalid automation.", "surface": "android", @@ -1171,7 +1171,7 @@ }, { "kind": "ui-call", - "line": 5977, + "line": 5978, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automation.", "surface": "android", @@ -1179,7 +1179,7 @@ }, { "kind": "ui-call", - "line": 5989, + "line": 5990, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect automation run history.", "surface": "android", @@ -1187,7 +1187,7 @@ }, { "kind": "ui-call", - "line": 6020, + "line": 6021, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load automation run history.", "surface": "android", @@ -1195,7 +1195,7 @@ }, { "kind": "ui-call", - "line": 6037, + "line": 6038, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron changes require operator.admin access.", "surface": "android", @@ -1203,7 +1203,7 @@ }, { "kind": "ui-call", - "line": 6046, + "line": 6047, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to manage automations.", "surface": "android", @@ -1211,7 +1211,7 @@ }, { "kind": "ui-call", - "line": 6058, + "line": 6059, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Another cron action is still finishing.", "surface": "android", @@ -1219,7 +1219,7 @@ }, { "kind": "ui-call", - "line": 6105, + "line": 6106, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron action failed.", "surface": "android", @@ -1227,7 +1227,7 @@ }, { "kind": "ui-call", - "line": 6217, + "line": 6218, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load usage.", "surface": "android", @@ -1235,7 +1235,7 @@ }, { "kind": "ui-call", - "line": 6232, + "line": 6233, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load skills.", "surface": "android", @@ -1243,7 +1243,7 @@ }, { "kind": "ui-call", - "line": 6252, + "line": 6253, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to update skills.", "surface": "android", @@ -1251,7 +1251,7 @@ }, { "kind": "ui-call", - "line": 6256, + "line": 6257, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "This gateway connection needs operator.admin to update skills.", "surface": "android", @@ -1259,7 +1259,7 @@ }, { "kind": "conditional-branch", - "line": 6271, + "line": 6272, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not disable skill.", "surface": "android", @@ -1267,7 +1267,7 @@ }, { "kind": "conditional-branch", - "line": 6271, + "line": 6272, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not enable skill.", "surface": "android", @@ -1275,7 +1275,7 @@ }, { "kind": "ui-call", - "line": 6289, + "line": 6290, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to search ClawHub skills.", "surface": "android", @@ -1283,7 +1283,7 @@ }, { "kind": "ui-call", - "line": 6335, + "line": 6336, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not search ClawHub skills.", "surface": "android", @@ -1291,7 +1291,7 @@ }, { "kind": "ui-call", - "line": 6348, + "line": 6349, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect ClawHub skills.", "surface": "android", @@ -1299,7 +1299,7 @@ }, { "kind": "conditional-branch", - "line": 6381, + "line": 6382, "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", @@ -1307,7 +1307,7 @@ }, { "kind": "ui-call", - "line": 6397, + "line": 6398, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load ClawHub details for ${skill.slug}.", "surface": "android", @@ -1315,7 +1315,7 @@ }, { "kind": "ui-call", - "line": 6413, + "line": 6414, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to install ClawHub skills.", "surface": "android", @@ -1323,7 +1323,7 @@ }, { "kind": "ui-call", - "line": 6429, + "line": 6430, "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", @@ -1331,7 +1331,7 @@ }, { "kind": "conditional-branch", - "line": 6519, + "line": 6520, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Installed $slug.", "surface": "android", @@ -1339,7 +1339,7 @@ }, { "kind": "ui-call", - "line": 6526, + "line": 6527, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not install ${slug} from ClawHub.", "surface": "android", @@ -1347,7 +1347,7 @@ }, { "kind": "ui-call", - "line": 6566, + "line": 6567, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to load Skill Workshop proposals.", "surface": "android", @@ -1355,7 +1355,7 @@ }, { "kind": "ui-call", - "line": 6606, + "line": 6607, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load Skill Workshop proposals.", "surface": "android", @@ -1363,7 +1363,7 @@ }, { "kind": "ui-call", - "line": 6626, + "line": 6627, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to inspect Skill Workshop proposals.", "surface": "android", @@ -1371,7 +1371,7 @@ }, { "kind": "ui-call", - "line": 6678, + "line": 6679, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not inspect Skill Workshop proposal.", "surface": "android", @@ -1379,7 +1379,7 @@ }, { "kind": "ui-call", - "line": 6698, + "line": 6699, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Skill Workshop proposal actions require operator.admin scope.", "surface": "android", @@ -1387,7 +1387,7 @@ }, { "kind": "ui-call", - "line": 6703, + "line": 6704, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect the gateway to update Skill Workshop proposals.", "surface": "android", @@ -1395,7 +1395,7 @@ }, { "kind": "ui-call", - "line": 6893, + "line": 6894, "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", @@ -1403,7 +1403,7 @@ }, { "kind": "ui-named-argument", - "line": 6975, + "line": 6976, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connected", "surface": "android", @@ -1411,7 +1411,7 @@ }, { "kind": "ui-call", - "line": 7005, + "line": 7006, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load nodes and devices.", "surface": "android", @@ -1419,7 +1419,7 @@ }, { "kind": "ui-call", - "line": 7655, + "line": 7656, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load channels.", "surface": "android", @@ -1427,7 +1427,7 @@ }, { "kind": "ui-call", - "line": 7673, + "line": 7674, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load dreaming.", "surface": "android", @@ -1435,7 +1435,7 @@ }, { "kind": "ui-call", - "line": 7688, + "line": 7689, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Could not load gateway logs.", "surface": "android", @@ -1443,7 +1443,7 @@ }, { "kind": "ui-call", - "line": 8171, + "line": 8172, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "One time", "surface": "android", @@ -1451,7 +1451,7 @@ }, { "kind": "ui-call", - "line": 8180, + "line": 8181, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Cron", "surface": "android", @@ -1459,7 +1459,7 @@ }, { "kind": "ui-call", - "line": 8181, + "line": 8182, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Scheduled", "surface": "android", @@ -1467,7 +1467,7 @@ }, { "kind": "ui-call", - "line": 8189, + "line": 8190, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${days}d", "surface": "android", @@ -1475,7 +1475,7 @@ }, { "kind": "ui-call", - "line": 8190, + "line": 8191, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${hours}h", "surface": "android", @@ -1483,7 +1483,7 @@ }, { "kind": "ui-call", - "line": 8191, + "line": 8192, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Every ${minutes}m", "surface": "android", @@ -1491,7 +1491,7 @@ }, { "kind": "ui-call", - "line": 8192, + "line": 8193, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Repeating", "surface": "android", @@ -1499,7 +1499,7 @@ }, { "kind": "ui-call", - "line": 8208, + "line": 8209, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "No prompt", "surface": "android", @@ -1507,7 +1507,7 @@ }, { "kind": "ui-call", - "line": 8225, + "line": 8226, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway", "surface": "android", @@ -1515,7 +1515,7 @@ }, { "kind": "ui-call", - "line": 8233, + "line": 8234, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connected to $gatewayLabel", "surface": "android", @@ -1523,7 +1523,7 @@ }, { "kind": "ui-call", - "line": 8234, + "line": 8235, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Your agents are ready", "surface": "android", @@ -1531,7 +1531,7 @@ }, { "kind": "ui-call", - "line": 8236, + "line": 8237, "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", @@ -1539,7 +1539,7 @@ }, { "kind": "ui-call", - "line": 8240, + "line": 8241, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Selected on this phone", "surface": "android", @@ -1547,7 +1547,7 @@ }, { "kind": "ui-call", - "line": 8243, + "line": 8244, "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", @@ -1555,7 +1555,7 @@ }, { "kind": "ui-call", - "line": 8248, + "line": 8249, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Reconnecting", "surface": "android", @@ -1563,7 +1563,7 @@ }, { "kind": "ui-call", - "line": 8249, + "line": 8250, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "OpenClaw is syncing back up", "surface": "android", @@ -1571,7 +1571,7 @@ }, { "kind": "ui-call", - "line": 8251, + "line": 8252, "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", @@ -1579,7 +1579,7 @@ }, { "kind": "ui-call", - "line": 8255, + "line": 8256, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Gateway session in progress", "surface": "android", @@ -1587,7 +1587,7 @@ }, { "kind": "ui-call", - "line": 8258, + "line": 8259, "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", @@ -1595,7 +1595,7 @@ }, { "kind": "ui-call", - "line": 8263, + "line": 8264, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Welcome to OpenClaw", "surface": "android", @@ -1603,7 +1603,7 @@ }, { "kind": "ui-call", - "line": 8264, + "line": 8265, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Your phone stays quiet until it is needed", "surface": "android", @@ -1611,7 +1611,7 @@ }, { "kind": "ui-call", - "line": 8266, + "line": 8267, "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", @@ -1619,7 +1619,7 @@ }, { "kind": "ui-call", - "line": 8270, + "line": 8271, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Connect to load your agents", "surface": "android", @@ -1627,7 +1627,7 @@ }, { "kind": "ui-call", - "line": 8273, + "line": 8274, "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", @@ -1635,7 +1635,7 @@ }, { "kind": "ui-call", - "line": 8305, + "line": 8306, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Main", "surface": "android", @@ -1643,7 +1643,7 @@ }, { "kind": "ui-call", - "line": 8320, + "line": 8321, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Active on this phone", "surface": "android", @@ -1651,7 +1651,7 @@ }, { "kind": "ui-call", - "line": 8321, + "line": 8322, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Default agent", "surface": "android", @@ -1659,7 +1659,7 @@ }, { "kind": "ui-call", - "line": 8322, + "line": 8323, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Ready", "surface": "android", @@ -1667,7 +1667,7 @@ }, { "kind": "ui-call", - "line": 8977, + "line": 8978, "path": "apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt", "source": "Dream", "surface": "android", @@ -1843,7 +1843,7 @@ }, { "kind": "ui-call", - "line": 1872, + "line": 1879, "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", @@ -1851,7 +1851,7 @@ }, { "kind": "ui-call", - "line": 2005, + "line": 2012, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not update model.", "surface": "android", @@ -1859,7 +1859,7 @@ }, { "kind": "ui-call", - "line": 2070, + "line": 2077, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not update thinking level.", "surface": "android", @@ -1867,7 +1867,7 @@ }, { "kind": "ui-call", - "line": 2584, + "line": 2592, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Chat failed before the run started; try again.", "surface": "android", @@ -1875,7 +1875,7 @@ }, { "kind": "ui-call", - "line": 4446, + "line": 4459, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not stage an attachment for sending.", "surface": "android", @@ -1883,7 +1883,7 @@ }, { "kind": "ui-call", - "line": 4479, + "line": 4492, "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", @@ -1891,7 +1891,7 @@ }, { "kind": "ui-call", - "line": 4485, + "line": 4498, "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", @@ -1899,7 +1899,7 @@ }, { "kind": "ui-call", - "line": 4491, + "line": 4504, "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", @@ -1907,7 +1907,7 @@ }, { "kind": "ui-call", - "line": 4496, + "line": 4509, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Gateway health not OK; cannot send", "surface": "android", @@ -1915,7 +1915,7 @@ }, { "kind": "ui-call", - "line": 4503, + "line": 4516, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Could not queue message for later delivery.", "surface": "android", @@ -1923,7 +1923,7 @@ }, { "kind": "ui-call", - "line": 5426, + "line": 5439, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Chat failed", "surface": "android", @@ -1931,7 +1931,7 @@ }, { "kind": "ui-call", - "line": 5730, + "line": 5760, "path": "apps/android/app/src/main/java/ai/openclaw/app/chat/ChatController.kt", "source": "Event stream interrupted; try refreshing.", "surface": "android", @@ -1939,7 +1939,7 @@ }, { "kind": "ui-call", - "line": 5934, + "line": 6051, "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", @@ -1947,7 +1947,7 @@ }, { "kind": "ui-call", - "line": 6151, + "line": 6268, "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", @@ -12875,7 +12875,7 @@ }, { "kind": "ui-call", - "line": 418, + "line": 424, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Model", "surface": "android", @@ -12883,7 +12883,7 @@ }, { "kind": "ui-call", - "line": 541, + "line": 547, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Some shared images were omitted or could not be added.", "surface": "android", @@ -12891,7 +12891,7 @@ }, { "kind": "ui-call", - "line": 697, + "line": 703, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat needs attention", "surface": "android", @@ -12899,7 +12899,7 @@ }, { "kind": "ui-call", - "line": 1047, + "line": 1054, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "All", "surface": "android", @@ -12907,7 +12907,7 @@ }, { "kind": "ui-call", - "line": 1127, + "line": 1134, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Show Sidebar", "surface": "android", @@ -12915,7 +12915,7 @@ }, { "kind": "ui-call", - "line": 1143, + "line": 1150, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Working", "surface": "android", @@ -12923,7 +12923,7 @@ }, { "kind": "ui-call", - "line": 1144, + "line": 1151, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Ready", "surface": "android", @@ -12931,7 +12931,7 @@ }, { "kind": "ui-call", - "line": 1145, + "line": 1152, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Offline", "surface": "android", @@ -12939,7 +12939,7 @@ }, { "kind": "ui-call", - "line": 1158, + "line": 1165, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat actions", "surface": "android", @@ -12947,7 +12947,7 @@ }, { "kind": "ui-call", - "line": 1163, + "line": 1170, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Refresh chat", "surface": "android", @@ -12955,7 +12955,7 @@ }, { "kind": "ui-call", - "line": 1182, + "line": 1189, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Dashboard", "surface": "android", @@ -12963,7 +12963,7 @@ }, { "kind": "ui-call", - "line": 1190, + "line": 1197, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Background tasks", "surface": "android", @@ -12971,7 +12971,7 @@ }, { "kind": "ui-call", - "line": 1211, + "line": 1218, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat", "surface": "android", @@ -12979,7 +12979,7 @@ }, { "kind": "ui-call", - "line": 1407, + "line": 1416, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Messages to recover", "surface": "android", @@ -12987,7 +12987,7 @@ }, { "kind": "ui-call", - "line": 1409, + "line": 1418, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "${item.count} message(s) need recovery. Re-enter anything you want to keep, then delete these rows.", "surface": "android", @@ -12995,7 +12995,7 @@ }, { "kind": "ui-call", - "line": 1454, + "line": 1468, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Loading thread", "surface": "android", @@ -13003,7 +13003,7 @@ }, { "kind": "ui-call", - "line": 1487, + "line": 1501, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Jump to latest", "surface": "android", @@ -13011,7 +13011,7 @@ }, { "kind": "ui-call", - "line": 1561, + "line": 1575, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Ready when you are", "surface": "android", @@ -13019,7 +13019,7 @@ }, { "kind": "ui-call", - "line": 1565, + "line": 1579, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Start with a prompt, or use voice.", "surface": "android", @@ -13027,7 +13027,7 @@ }, { "kind": "ui-call", - "line": 1567, + "line": 1581, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Use the recovery options below to reconnect.", "surface": "android", @@ -13035,7 +13035,7 @@ }, { "kind": "ui-call", - "line": 1569, + "line": 1583, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat is checking Gateway health.", "surface": "android", @@ -13043,7 +13043,7 @@ }, { "kind": "ui-call", - "line": 1592, + "line": 1606, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Fix connection", "surface": "android", @@ -13051,7 +13051,7 @@ }, { "kind": "ui-call", - "line": 1593, + "line": 1607, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Copy diagnostics", "surface": "android", @@ -13059,7 +13059,7 @@ }, { "kind": "ui-call", - "line": 1652, + "line": 1666, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Catch me up", "surface": "android", @@ -13067,7 +13067,7 @@ }, { "kind": "ui-call", - "line": 1653, + "line": 1667, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Summarize recent threads and next steps.", "surface": "android", @@ -13075,7 +13075,7 @@ }, { "kind": "ui-call", - "line": 1654, + "line": 1668, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Catch me up on my recent OpenClaw threads and suggest next steps.", "surface": "android", @@ -13083,7 +13083,7 @@ }, { "kind": "ui-call", - "line": 1658, + "line": 1672, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Plan the work", "surface": "android", @@ -13091,7 +13091,7 @@ }, { "kind": "ui-call", - "line": 1659, + "line": 1673, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Turn a goal into an actionable checklist.", "surface": "android", @@ -13099,7 +13099,7 @@ }, { "kind": "ui-call", - "line": 1660, + "line": 1674, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Help me turn this goal into a practical checklist: ", "surface": "android", @@ -13107,7 +13107,7 @@ }, { "kind": "ui-call", - "line": 1664, + "line": 1678, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Use this phone", "surface": "android", @@ -13115,7 +13115,7 @@ }, { "kind": "ui-call", - "line": 1665, + "line": 1679, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Ask OpenClaw to use Android capabilities.", "surface": "android", @@ -13123,7 +13123,7 @@ }, { "kind": "ui-call", - "line": 1666, + "line": 1680, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "What can you help me do from this phone right now?", "surface": "android", @@ -13131,7 +13131,7 @@ }, { "kind": "ui-call", - "line": 1750, + "line": 1764, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw · Live", "surface": "android", @@ -13139,7 +13139,7 @@ }, { "kind": "ui-call", - "line": 1751, + "line": 1765, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "You", "surface": "android", @@ -13147,7 +13147,7 @@ }, { "kind": "ui-call", - "line": 1752, + "line": 1766, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "System", "surface": "android", @@ -13155,7 +13155,7 @@ }, { "kind": "ui-call", - "line": 1753, + "line": 1767, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw", "surface": "android", @@ -13163,7 +13163,7 @@ }, { "kind": "ui-call", - "line": 1792, + "line": 1806, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Image", "surface": "android", @@ -13171,7 +13171,7 @@ }, { "kind": "ui-call", - "line": 1808, + "line": 1822, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Additional images hidden: ${omittedImageCount}", "surface": "android", @@ -13179,7 +13179,7 @@ }, { "kind": "ui-call", - "line": 1863, + "line": 1877, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Preparing audio…", "surface": "android", @@ -13187,7 +13187,7 @@ }, { "kind": "ui-call", - "line": 1863, + "line": 1877, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Speaking…", "surface": "android", @@ -13195,7 +13195,7 @@ }, { "kind": "ui-call", - "line": 1894, + "line": 1908, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Close", "surface": "android", @@ -13203,7 +13203,7 @@ }, { "kind": "ui-call", - "line": 1894, + "line": 1908, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "View all", "surface": "android", @@ -13211,7 +13211,7 @@ }, { "kind": "ui-call", - "line": 1924, + "line": 1938, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Tools running", "surface": "android", @@ -13219,7 +13219,7 @@ }, { "kind": "ui-call", - "line": 1926, + "line": 1942, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "OpenClaw is working", "surface": "android", @@ -13227,15 +13227,71 @@ }, { "kind": "ui-call", - "line": 1929, + "line": 1947, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "+${toolCalls.size - 4} more", "surface": "android", "id": "native.android.0fd6b75cadc0ce75" }, + { + "kind": "ui-call", + "line": 1967, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "+${moreWorkingCount} more working", + "surface": "android", + "id": "native.android.f2da5ffc1adaad58" + }, + { + "kind": "ui-call", + "line": 2038, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "+${diff.added}", + "surface": "android", + "id": "native.android.db391c2712f7340b" + }, + { + "kind": "ui-call", + "line": 2041, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "−${diff.removed}", + "surface": "android", + "id": "native.android.babb72da2d6270a2" + }, + { + "kind": "ui-call", + "line": 2066, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "Subagent working", + "surface": "android", + "id": "native.android.e58b3c8805c79bd0" + }, + { + "kind": "ui-call", + "line": 2068, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "Subagent failed", + "surface": "android", + "id": "native.android.bdfea93e16940c82" + }, + { + "kind": "ui-call", + "line": 2069, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "Subagent cancelled", + "surface": "android", + "id": "native.android.9fde542e43b4e73b" + }, + { + "kind": "ui-call", + "line": 2070, + "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", + "source": "Subagent finished", + "surface": "android", + "id": "native.android.0bba48c12caa076a" + }, { "kind": "ui-named-argument", - "line": 1995, + "line": 2133, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$completedCount/${steps.size}", "surface": "android", @@ -13243,7 +13299,7 @@ }, { "kind": "ui-call", - "line": 2002, + "line": 2140, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Collapse plan checklist", "surface": "android", @@ -13251,7 +13307,7 @@ }, { "kind": "ui-call", - "line": 2002, + "line": 2140, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Expand plan checklist", "surface": "android", @@ -13259,7 +13315,7 @@ }, { "kind": "ui-call", - "line": 2135, + "line": 2273, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Dismiss shared-image warning", "surface": "android", @@ -13267,7 +13323,7 @@ }, { "kind": "ui-call", - "line": 2239, + "line": 2377, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Stop", "surface": "android", @@ -13275,7 +13331,7 @@ }, { "kind": "ui-call", - "line": 2326, + "line": 2464, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Switch branch", "surface": "android", @@ -13283,7 +13339,7 @@ }, { "kind": "ui-call", - "line": 2350, + "line": 2488, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Untitled branch", "surface": "android", @@ -13291,7 +13347,7 @@ }, { "kind": "ui-call", - "line": 2365, + "line": 2503, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Current branch", "surface": "android", @@ -13299,7 +13355,7 @@ }, { "kind": "ui-call", - "line": 2377, + "line": 2515, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Messages: $count", "surface": "android", @@ -13307,7 +13363,7 @@ }, { "kind": "ui-call", - "line": 2385, + "line": 2523, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$count · $updated", "surface": "android", @@ -13315,7 +13371,7 @@ }, { "kind": "ui-call", - "line": 2414, + "line": 2552, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Default", "surface": "android", @@ -13323,7 +13379,7 @@ }, { "kind": "ui-call", - "line": 2480, + "line": 2618, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Pin model", "surface": "android", @@ -13331,7 +13387,7 @@ }, { "kind": "ui-call", - "line": 2480, + "line": 2618, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Unpin model", "surface": "android", @@ -13339,7 +13395,7 @@ }, { "kind": "ui-call", - "line": 2497, + "line": 2635, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "No commands found", "surface": "android", @@ -13347,7 +13403,7 @@ }, { "kind": "ui-call", - "line": 2539, + "line": 2677, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Command", "surface": "android", @@ -13355,7 +13411,7 @@ }, { "kind": "ui-call", - "line": 2559, + "line": 2697, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Gateway offline", "surface": "android", @@ -13363,7 +13419,7 @@ }, { "kind": "ui-call", - "line": 2605, + "line": 2743, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Close thinking level selector", "surface": "android", @@ -13371,7 +13427,7 @@ }, { "kind": "ui-call", - "line": 2605, + "line": 2743, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Open thinking level selector", "surface": "android", @@ -13379,7 +13435,7 @@ }, { "kind": "ui-call", - "line": 2671, + "line": 2809, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Attach image", "surface": "android", @@ -13387,7 +13443,7 @@ }, { "kind": "ui-call", - "line": 2676, + "line": 2814, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Attachment", "surface": "android", @@ -13395,7 +13451,7 @@ }, { "kind": "ui-call", - "line": 2681, + "line": 2819, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Attach video", "surface": "android", @@ -13403,7 +13459,7 @@ }, { "kind": "ui-call", - "line": 2712, + "line": 2850, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Message OpenClaw", "surface": "android", @@ -13411,7 +13467,7 @@ }, { "kind": "ui-call", - "line": 2741, + "line": 2879, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "End Talk", "surface": "android", @@ -13419,7 +13475,7 @@ }, { "kind": "ui-call", - "line": 2741, + "line": 2879, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Start Talk", "surface": "android", @@ -13427,7 +13483,7 @@ }, { "kind": "ui-call", - "line": 2841, + "line": 2979, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Voice note · ${formatVoiceNoteDuration(duration)}", "surface": "android", @@ -13435,7 +13491,7 @@ }, { "kind": "ui-call", - "line": 2850, + "line": 2988, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Remove attachment", "surface": "android", @@ -13443,7 +13499,7 @@ }, { "kind": "ui-call", - "line": 2862, + "line": 3000, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "New chat", "surface": "android", @@ -13451,7 +13507,7 @@ }, { "kind": "ui-call", - "line": 2871, + "line": 3009, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Main", "surface": "android", @@ -13459,7 +13515,7 @@ }, { "kind": "ui-call", - "line": 2872, + "line": 3010, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Current", "surface": "android", @@ -13467,7 +13523,7 @@ }, { "kind": "ui-call", - "line": 2879, + "line": 3017, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$emoji $name", "surface": "android", @@ -13475,7 +13531,7 @@ }, { "kind": "ui-call", - "line": 2938, + "line": 3076, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Send", "surface": "android", @@ -13483,7 +13539,7 @@ }, { "kind": "ui-call", - "line": 2949, + "line": 3087, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Chat is still checking Gateway health.", "surface": "android", @@ -13491,7 +13547,7 @@ }, { "kind": "ui-call", - "line": 2950, + "line": 3088, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Gateway is offline. Fix the connection below or copy diagnostics.", "surface": "android", @@ -13499,7 +13555,7 @@ }, { "kind": "ui-call", - "line": 2951, + "line": 3089, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Gateway authentication needs attention.", "surface": "android", @@ -13507,7 +13563,7 @@ }, { "kind": "ui-call", - "line": 2970, + "line": 3108, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Context ${(it * 100).roundToInt()}%", "surface": "android", @@ -13515,7 +13571,7 @@ }, { "kind": "ui-call", - "line": 2971, + "line": 3109, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Context --", "surface": "android", @@ -13523,7 +13579,7 @@ }, { "kind": "ui-call", - "line": 2972, + "line": 3110, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "$contextLabel · ${contextMeterThinkingLabel(thinkingLevel)}", "surface": "android", @@ -13531,7 +13587,7 @@ }, { "kind": "ui-call", - "line": 3011, + "line": 3149, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Off", "surface": "android", @@ -13539,7 +13595,7 @@ }, { "kind": "ui-call", - "line": 3012, + "line": 3150, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Minimal", "surface": "android", @@ -13547,7 +13603,7 @@ }, { "kind": "ui-call", - "line": 3013, + "line": 3151, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Low", "surface": "android", @@ -13555,7 +13611,7 @@ }, { "kind": "ui-call", - "line": 3014, + "line": 3152, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Medium", "surface": "android", @@ -13563,7 +13619,7 @@ }, { "kind": "ui-call", - "line": 3015, + "line": 3153, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "High", "surface": "android", @@ -13571,7 +13627,7 @@ }, { "kind": "ui-call", - "line": 3016, + "line": 3154, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Xhigh", "surface": "android", @@ -13579,7 +13635,7 @@ }, { "kind": "ui-call", - "line": 3017, + "line": 3155, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Adaptive", "surface": "android", @@ -13587,7 +13643,7 @@ }, { "kind": "ui-call", - "line": 3018, + "line": 3156, "path": "apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt", "source": "Max", "surface": "android", diff --git a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt index 43eff3dc507a..5223547e211c 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/MainViewModel.kt @@ -671,6 +671,8 @@ class MainViewModel private constructor( val chatModelCatalog: StateFlow> = runtimeState(initial = emptyList()) { it.chatModelCatalog } val chatStreamingAssistantText: StateFlow = runtimeState(initial = null) { it.chatStreamingAssistantText } val chatPendingToolCalls: StateFlow> = runtimeState(initial = emptyList()) { it.chatPendingToolCalls } + val chatSubagentActivities: StateFlow> = + runtimeState(initial = emptyMap()) { it.chatSubagentActivities } val chatQuestions: StateFlow> = runtimeState(initial = emptyList()) { it.chatQuestions } val chatPlanSteps: StateFlow> = runtimeState(initial = emptyList()) { it.chatPlanSteps } val chatSessions: StateFlow> = runtimeState(initial = emptyList()) { it.chatSessions } 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 9c58be3101ce..af02f584ee41 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 @@ -2879,6 +2879,7 @@ class NodeRuntime private constructor( val chatModelCatalog: StateFlow> = chat.modelCatalog val chatStreamingAssistantText: StateFlow = chat.streamingAssistantText val chatPendingToolCalls: StateFlow> = chat.pendingToolCalls + val chatSubagentActivities: StateFlow> = chat.subagentActivities val chatQuestions: StateFlow> = chat.questions val chatPlanSteps: StateFlow> = chat.planSteps val chatSessions: StateFlow> = chat.sessions diff --git a/apps/android/app/src/main/java/ai/openclaw/app/chat/BackgroundTask.kt b/apps/android/app/src/main/java/ai/openclaw/app/chat/BackgroundTask.kt index ca19e79bc06d..d39672d2f65a 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/chat/BackgroundTask.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/chat/BackgroundTask.kt @@ -21,6 +21,7 @@ data class BackgroundTask( val updatedAtMs: Long?, val startedAtMs: Long?, val endedAtMs: Long?, + val lastActivity: String? = null, val progress: String?, val terminal: String?, val error: String?, @@ -46,9 +47,9 @@ data class BackgroundTask( get() { val candidates = if (status == "failed" || status == "timed_out") { - listOf(error, terminal, progress) + listOf(error, terminal, lastActivity, progress) } else { - listOf(terminal, error, progress) + listOf(terminal, error, lastActivity, progress) } return candidates.firstOrNull { !it.isNullOrBlank() } } @@ -89,6 +90,7 @@ internal fun parseBackgroundTask(element: JsonElement): BackgroundTask? { updatedAtMs = objectValue["updatedAt"]?.let(::parseTaskTimestampMs), startedAtMs = objectValue["startedAt"]?.let(::parseTaskTimestampMs), endedAtMs = objectValue["endedAt"]?.let(::parseTaskTimestampMs), + lastActivity = string("lastActivity"), progress = string("progressSummary"), terminal = string("terminalSummary"), error = string("error"), @@ -105,7 +107,7 @@ internal fun mergeBackgroundTasks(vararg groups: List): List { it.isActive }.thenByDescending { it.activityAtMs }) -private fun parseTaskTimestampMs(element: JsonElement): Long? { +internal fun parseTaskTimestampMs(element: JsonElement): Long? { val primitive = element.jsonPrimitive primitive.doubleOrNull?.let { return it.toLong() } return primitive.contentOrNull?.let { raw -> runCatching { Instant.parse(raw).toEpochMilli() }.getOrNull() } 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 b909fb39001c..24c311ea1cc0 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 @@ -58,6 +58,7 @@ import java.util.concurrent.atomic.AtomicLong internal const val SESSION_LIST_FETCH_LIMIT = 200 private val QUESTION_REFRESH_RETRY_DELAYS_MS = longArrayOf(1_000L, 2_000L, 4_000L) private val SWARM_REFRESH_RETRY_DELAYS_MS = longArrayOf(1_000L, 2_000L, 4_000L) +private const val SUBAGENT_ACTIVITY_RETENTION_MS = 60_000L private const val SESSION_EDITOR_MAX_BASE64_CHARS = ((OUTBOX_MAX_COMMAND_ATTACHMENT_BYTES + 2) / 3) * 4 private val MANAGED_MEDIA_PATH_REGEX = Regex("^/api/chat/media/outgoing/[^/]+/([0-9a-fA-F-]{36})/full(?:\\?.*)?$") @@ -327,6 +328,11 @@ class ChatController internal constructor( private val _pendingToolCalls = MutableStateFlow>(emptyList()) val pendingToolCalls: StateFlow> = _pendingToolCalls.asStateFlow() + private val subagentActivityLock = Any() + private val subagentActivityExpiryJobs = mutableMapOf() + private val _subagentActivities = MutableStateFlow>(emptyMap()) + val subagentActivities: StateFlow> = _subagentActivities.asStateFlow() + private val _questions = MutableStateFlow>(emptyList()) val questions: StateFlow> = _questions.asStateFlow() private val questionStateLock = Any() @@ -694,6 +700,7 @@ class ChatController internal constructor( clearMessages = true, markLoading = false, ) + clearSubagentActivities() clearLiveHistoryMarker() _sessions.value = emptyList() publishRunPresentation() @@ -2225,6 +2232,7 @@ class ChatController internal constructor( _sessionBranches.value = emptyList() _sessionBranchesLoading.value = false _sessionBranchSwitching.value = false + clearSubagentActivities() } _sessionKey.value = key _sessionOwnerAgentId.value = owner @@ -3096,6 +3104,7 @@ class ChatController internal constructor( invalidateIncompleteRunTelemetry() publishRunPresentation() clearLiveRunUi() + clearSubagentActivities() refreshQuestions() refreshHistoryForRecovery() } @@ -3122,6 +3131,10 @@ class ChatController internal constructor( if (payloadJson.isNullOrBlank()) return handleAgentEvent(payloadJson) } + "task" -> { + if (payloadJson.isNullOrBlank()) return + handleTaskEvent(payloadJson) + } "question.requested" -> { if (payloadJson.isNullOrBlank()) return handleQuestionRequested(payloadJson) @@ -5704,20 +5717,37 @@ class ChatController internal constructor( if (phase.isNullOrEmpty() || name.isNullOrEmpty() || toolCallId.isNullOrEmpty()) return val ts = payload["ts"].asLongOrNull() ?: System.currentTimeMillis() - if (phase == "start") { - val args = data.get("args").asObjectOrNull() - pendingToolCallsById[toolCallId] = - ChatPendingToolCall( - toolCallId = toolCallId, - name = name, - args = args, - startedAtMs = ts, - isError = null, - ) - publishPendingToolCalls() - } else if (phase == "result") { - pendingToolCallsById.remove(toolCallId) - publishPendingToolCalls() + when (phase) { + "start" -> { + val existing = pendingToolCallsById[toolCallId] + pendingToolCallsById[toolCallId] = + ChatPendingToolCall( + toolCallId = toolCallId, + name = name, + args = data.get("args").asObjectOrNull(), + startedAtMs = existing?.startedAtMs ?: ts, + isError = null, + liveDiff = existing?.liveDiff, + ) + publishPendingToolCalls() + } + "input_delta" -> { + val diff = parseChatDiffStat(data["diff"], includeFiles = false) ?: return + val existing = pendingToolCallsById[toolCallId] + pendingToolCallsById[toolCallId] = + existing?.copy(name = name, liveDiff = diff) + ?: ChatPendingToolCall( + toolCallId = toolCallId, + name = name, + startedAtMs = ts, + liveDiff = diff, + ) + publishPendingToolCalls() + } + "result" -> { + pendingToolCallsById.remove(toolCallId) + publishPendingToolCalls() + } } } "plan" -> { @@ -5819,6 +5849,93 @@ class ChatController internal constructor( pendingToolCallsById.values.sortedBy { it.startedAtMs } } + private fun handleTaskEvent(payloadJson: String) { + val payload = json.parseToJsonElement(payloadJson).asObjectOrNull() ?: return + if (payload["action"].asStringOrNull() == "deleted") { + payload["taskId"] + .asStringOrNull() + ?.trim() + ?.takeIf(String::isNotEmpty) + ?.let(::removeSubagentActivity) + return + } + val task = payload["task"].asObjectOrNull() ?: return + if (task["runtime"].asStringOrNull() != "subagent") return + if (task["sessionKey"].asStringOrNull()?.trim() != _sessionKey.value) return + val taskId = task["id"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) ?: return + val status = task["status"].asStringOrNull()?.trim()?.lowercase() ?: return + if (status !in setOf("queued", "running", "completed", "failed", "cancelled", "timed_out")) return + + val terminal = status != "queued" && status != "running" + val now = System.currentTimeMillis() + synchronized(subagentActivityLock) { + val existing = _subagentActivities.value[taskId] + val lastActivity = task["lastActivity"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + val fallback = + task["progressSummary"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + ?: task["lastToolName"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + val activity = + ChatSubagentActivity( + id = taskId, + status = status, + snippet = + lastActivity + ?: if (terminal) existing?.snippet ?: fallback else fallback ?: existing?.snippet, + diffStat = parseChatDiffStat(task["diffStat"], includeFiles = true) ?: existing?.diffStat, + terminalSummary = + task["terminalSummary"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + ?: existing?.terminalSummary, + error = + task["error"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + ?: existing?.error, + startedAtMs = + task["startedAt"]?.let(::parseTaskTimestampMs) + ?: existing?.startedAtMs + ?: task["createdAt"]?.let(::parseTaskTimestampMs) + ?: now, + endedAtMs = + if (terminal) { + task["endedAt"]?.let(::parseTaskTimestampMs) ?: existing?.endedAtMs ?: now + } else { + null + }, + childSessionKey = + task["childSessionKey"].asStringOrNull()?.trim()?.takeIf(String::isNotEmpty) + ?: existing?.childSessionKey, + ) + _subagentActivities.value = _subagentActivities.value + (taskId to activity) + subagentActivityExpiryJobs.remove(taskId)?.cancel() + if (!activity.isWorking) { + subagentActivityExpiryJobs[taskId] = + scope.launch { + val expiresAt = (activity.endedAtMs ?: now) + SUBAGENT_ACTIVITY_RETENTION_MS + delay((expiresAt - System.currentTimeMillis()).coerceAtLeast(0L)) + synchronized(subagentActivityLock) { + if (_subagentActivities.value[taskId] == activity) { + _subagentActivities.value = _subagentActivities.value - taskId + } + subagentActivityExpiryJobs.remove(taskId) + } + } + } + } + } + + private fun removeSubagentActivity(taskId: String) { + synchronized(subagentActivityLock) { + subagentActivityExpiryJobs.remove(taskId)?.cancel() + _subagentActivities.value = _subagentActivities.value - taskId + } + } + + private fun clearSubagentActivities() { + synchronized(subagentActivityLock) { + subagentActivityExpiryJobs.values.forEach(Job::cancel) + subagentActivityExpiryJobs.clear() + _subagentActivities.value = emptyMap() + } + } + private fun clearLiveRunUi() { pendingToolCallsById.clear() publishPendingToolCalls() @@ -7186,6 +7303,26 @@ private fun JsonElement?.asBooleanOrNull(): Boolean? = else -> null } +private fun parseChatDiffStat( + element: JsonElement?, + includeFiles: Boolean, +): ChatDiffStat? { + val value = element.asObjectOrNull() ?: return null + + fun count(key: String): Int? = + value[key] + .asLongOrNull() + ?.takeIf { it in 0..Int.MAX_VALUE.toLong() } + ?.toInt() + + val files = if (includeFiles) count("files") ?: return null else null + return ChatDiffStat( + added = count("added") ?: return null, + removed = count("removed") ?: return null, + files = files, + ) +} + internal fun resolvePreferredActiveRunId( localRunIds: Collection, advertisedRunIds: List, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt index 2c507ba6e998..62b47ec00fd8 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/chat/ChatModels.kt @@ -102,8 +102,30 @@ data class ChatPendingToolCall( val args: kotlinx.serialization.json.JsonObject? = null, val startedAtMs: Long, val isError: Boolean? = null, + val liveDiff: ChatDiffStat? = null, ) +data class ChatDiffStat( + val added: Int, + val removed: Int, + val files: Int? = null, +) + +data class ChatSubagentActivity( + val id: String, + val status: String, + val snippet: String?, + val diffStat: ChatDiffStat?, + val terminalSummary: String?, + val error: String?, + val startedAtMs: Long, + val endedAtMs: Long?, + val childSessionKey: String?, +) { + val isWorking: Boolean + get() = status == "queued" || status == "running" +} + enum class ChatPlanStepStatus { Pending, InProgress, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt index 49230623d4c7..d88205793a88 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatScreen.kt @@ -11,6 +11,7 @@ import ai.openclaw.app.SHARED_AUDIO_DOCUMENT_MIME_TYPES import ai.openclaw.app.SHARED_VIDEO_MIME_TYPES import ai.openclaw.app.chat.ChatCommandEntry import ai.openclaw.app.chat.ChatComposerOwner +import ai.openclaw.app.chat.ChatDiffStat import ai.openclaw.app.chat.ChatMessage import ai.openclaw.app.chat.ChatMessageContent import ai.openclaw.app.chat.ChatOutboxItem @@ -20,6 +21,7 @@ import ai.openclaw.app.chat.ChatPlanStep import ai.openclaw.app.chat.ChatPlanStepStatus import ai.openclaw.app.chat.ChatQuestionPrompt import ai.openclaw.app.chat.ChatSessionEntry +import ai.openclaw.app.chat.ChatSubagentActivity import ai.openclaw.app.chat.ChatThinkingLevelOption import ai.openclaw.app.chat.ChatThinkingLevelSelection import ai.openclaw.app.chat.ChatTranscriptAnchorState @@ -63,9 +65,12 @@ import ai.openclaw.app.ui.gatewayStatusForDisplay import ai.openclaw.app.ui.localizedUppercase import ai.openclaw.app.ui.mobileCallout import ai.openclaw.app.ui.relativeSessionTime +import ai.openclaw.app.ui.rememberSystemAnimationsEnabled import android.os.SystemClock import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.animateFloat @@ -288,6 +293,7 @@ fun ChatScreen( val thinkingLevelSelection by viewModel.chatThinkingLevelSelection.collectAsState() val streamingAssistantText by viewModel.chatStreamingAssistantText.collectAsState() val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState() + val subagentActivities by viewModel.chatSubagentActivities.collectAsState() val questions by viewModel.chatQuestions.collectAsState() val planSteps by viewModel.chatPlanSteps.collectAsState() val sessions by viewModel.chatSessions.collectAsState() @@ -710,6 +716,7 @@ fun ChatScreen( activeRunClockKey = selectedActiveRun.clockKey, activeRunOutputTokens = selectedActiveRun.outputTokens, pendingToolCalls = pendingToolCalls, + subagentActivities = subagentActivities, questions = questionsForSession(questions, sessionKey, mainSessionKey, activeAgentId), streamingAssistantText = streamingAssistantText, healthOk = healthOk, @@ -1286,6 +1293,7 @@ private fun ChatMessageList( activeRunClockKey: String?, activeRunOutputTokens: Long?, pendingToolCalls: List, + subagentActivities: Map, questions: List, streamingAssistantText: String?, healthOk: Boolean, @@ -1310,12 +1318,13 @@ private fun ChatMessageList( modifier: Modifier = Modifier, ) { val baseTimeline = - remember(messages, activeRunCount, pendingToolCalls, questions, streamingAssistantText, outboxItems, recoveryOutboxItems) { + remember(messages, activeRunCount, pendingToolCalls, subagentActivities, questions, streamingAssistantText, outboxItems, recoveryOutboxItems) { buildChatTimeline( messages = messages, pendingRunCount = activeRunCount, pendingToolCalls = pendingToolCalls, streamingAssistantText = streamingAssistantText, + subagentActivities = subagentActivities, outboxItems = outboxItems, recoveryOutboxItems = recoveryOutboxItems, questions = questions, @@ -1412,6 +1421,11 @@ private fun ChatMessageList( ), ) is ChatTimelineItem.PendingTools -> ToolBubble(toolCalls = item.toolCalls) + is ChatTimelineItem.SubagentActivity -> + SubagentActivityRows( + activities = item.activities, + moreWorkingCount = item.moreWorkingCount, + ) is ChatTimelineItem.QuestionPrompt -> ChatQuestionCard(prompt = item.prompt, onSubmit = onResolveQuestion, onSkip = onSkipQuestion) is ChatTimelineItem.TurnRecapSummary -> ChatTurnRecapRow(item.recap) @@ -1923,7 +1937,11 @@ private fun ToolBubble(toolCalls: List) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { ClawStatusPill(text = nativeString("Tools running"), status = ClawStatus.Warning) toolCalls.take(4).forEach { tool -> - ClawListItem(title = tool.name, subtitle = nativeString("OpenClaw is working")) + ClawListItem( + title = tool.name, + subtitle = nativeString("OpenClaw is working"), + trailing = { tool.liveDiff?.let { DiffStatChips(it) } }, + ) } if (toolCalls.size > 4) { Text(text = nativeString("+\${toolCalls.size - 4} more", toolCalls.size - 4), style = ClawTheme.type.caption, color = ClawTheme.colors.textSubtle) @@ -1932,6 +1950,126 @@ private fun ToolBubble(toolCalls: List) { } } +@Composable +private fun SubagentActivityRows( + activities: List, + moreWorkingCount: Int, +) { + val animationsEnabled = rememberSystemAnimationsEnabled() + ClawPanel { + Column( + modifier = if (animationsEnabled) Modifier.animateContentSize() else Modifier, + verticalArrangement = Arrangement.spacedBy(5.dp), + ) { + activities.forEach { activity -> SubagentActivityRow(activity, animationsEnabled) } + if (moreWorkingCount > 0) { + Text( + text = nativeString("+\${moreWorkingCount} more working", moreWorkingCount), + style = ClawTheme.type.caption, + color = ClawTheme.colors.textSubtle, + ) + } + } + } +} + +@Composable +private fun SubagentActivityRow( + activity: ChatSubagentActivity, + animationsEnabled: Boolean, +) { + val completed = activity.status == "completed" + val summary = if (activity.isWorking) activity.snippet else activity.terminalSummary ?: activity.error ?: activity.snippet + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + if (activity.isWorking) { + WorkingClawIcon(runKey = activity.id, color = ClawTheme.colors.primary) + } else { + Icon( + imageVector = if (completed) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + modifier = Modifier.size(15.dp), + tint = if (completed) ClawTheme.colors.success else ClawTheme.colors.danger, + ) + } + Text( + text = subagentActivityStatusLabel(activity.status), + style = ClawTheme.type.label, + color = ClawTheme.colors.text, + maxLines = 1, + ) + if (summary.isNullOrBlank()) { + Box(modifier = Modifier.weight(1f)) + } else if (animationsEnabled) { + AnimatedContent( + targetState = summary, + modifier = Modifier.weight(1f), + ) { text -> + SubagentActivitySnippet(text) + } + } else { + SubagentActivitySnippet(summary, Modifier.weight(1f)) + } + activity.diffStat?.takeIf { it.added > 0 || it.removed > 0 }?.let { DiffStatChips(it) } + } +} + +@Composable +private fun SubagentActivitySnippet( + text: String, + modifier: Modifier = Modifier, +) { + Text( + text = text, + modifier = modifier, + style = ClawTheme.type.caption, + color = ClawTheme.colors.textMuted, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun DiffStatChips(diff: ChatDiffStat) { + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (diff.added > 0) { + DiffStatChip(text = nativeString("+\${diff.added}", diff.added), color = ClawTheme.colors.success, background = ClawTheme.colors.successSoft) + } + if (diff.removed > 0) { + DiffStatChip(text = nativeString("−\${diff.removed}", diff.removed), color = ClawTheme.colors.danger, background = ClawTheme.colors.dangerSoft) + } + } +} + +@Composable +private fun DiffStatChip( + text: String, + color: Color, + background: Color, +) { + Surface(shape = RoundedCornerShape(ClawTheme.radii.control), color = background) { + Text( + text = text, + modifier = Modifier.padding(horizontal = 5.dp, vertical = 1.dp), + style = ClawTheme.type.caption.copy(fontWeight = FontWeight.SemiBold), + color = color, + maxLines = 1, + ) + } +} + +@Composable +private fun subagentActivityStatusLabel(status: String): String = + when (status) { + "queued", "running" -> nativeString("Subagent working") + "completed" -> nativeString("Subagent finished") + "failed", "timed_out" -> nativeString("Subagent failed") + "cancelled" -> nativeString("Subagent cancelled") + else -> nativeString("Subagent finished") + } + @Composable private fun ChatNotice( title: String, diff --git a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt index f38864fe0bf4..e1776f579415 100644 --- a/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt +++ b/apps/android/app/src/main/java/ai/openclaw/app/ui/chat/ChatTimeline.kt @@ -5,6 +5,7 @@ import ai.openclaw.app.chat.ChatOutboxItem import ai.openclaw.app.chat.ChatOutboxStatus import ai.openclaw.app.chat.ChatPendingToolCall import ai.openclaw.app.chat.ChatQuestionPrompt +import ai.openclaw.app.chat.ChatSubagentActivity import ai.openclaw.app.chat.OUTBOX_OWNER_CHANGED_ERROR import ai.openclaw.app.resolveAgentIdFromMainSessionKey @@ -35,6 +36,11 @@ internal sealed class ChatTimelineItem { val toolCalls: List, ) : ChatTimelineItem() + data class SubagentActivity( + val activities: List, + val moreWorkingCount: Int = 0, + ) : ChatTimelineItem() + data class QuestionPrompt( val prompt: ChatQuestionPrompt, ) : ChatTimelineItem() @@ -60,11 +66,13 @@ internal fun buildChatTimeline( pendingRunCount: Int, pendingToolCalls: List, streamingAssistantText: String?, + subagentActivities: Map = emptyMap(), outboxItems: List = emptyList(), recoveryOutboxItems: List = emptyList(), questions: List = emptyList(), ): ChatTimeline { val stream = streamingAssistantText?.trim()?.takeIf { it.isNotEmpty() } + val visibleSubagents = visibleSubagentActivities(subagentActivities.values) val items = buildList { // reverseLayout: index 0 renders bottom-most; queued commands are the newest user input. @@ -74,6 +82,14 @@ internal fun buildChatTimeline( if (recoveryOutboxItems.isNotEmpty()) add(ChatTimelineItem.OutboxRecoveryHeader(recoveryOutboxItems.size)) if (stream != null) add(ChatTimelineItem.StreamingAssistant(stream)) if (pendingToolCalls.isNotEmpty()) add(ChatTimelineItem.PendingTools(pendingToolCalls)) + if (visibleSubagents.activities.isNotEmpty()) { + add( + ChatTimelineItem.SubagentActivity( + activities = visibleSubagents.activities, + moreWorkingCount = visibleSubagents.moreWorkingCount, + ), + ) + } if (pendingRunCount > 0) add(ChatTimelineItem.Thinking) messages.asReversed().forEach { message -> add(ChatTimelineItem.Message(message)) } } @@ -114,6 +130,8 @@ internal fun buildChatTimeline( messages, pendingRunCount, pendingToolCalls, + visibleSubagents.activities, + visibleSubagents.moreWorkingCount, stream, outboxItems + recoveryOutboxItems, questions, @@ -216,6 +234,8 @@ private fun latestContentVersion( messages: List, pendingRunCount: Int, pendingToolCalls: List, + subagentActivities: Collection, + moreWorkingCount: Int, stream: String?, outboxItems: List = emptyList(), questions: List = emptyList(), @@ -252,8 +272,27 @@ private fun latestContentVersion( append(call.name) append(',') append(call.isError) + append(',') + append(call.liveDiff) append(';') } + append(":subagents=") + subagentActivities.sortedBy { it.id }.forEach { activity -> + append(activity.id) + append(',') + append(activity.status) + append(',') + append(activity.snippet?.hashCode() ?: 0) + append(',') + append(activity.terminalSummary?.hashCode() ?: 0) + append(',') + append(activity.error?.hashCode() ?: 0) + append(',') + append(activity.diffStat) + append(';') + } + append("more=") + append(moreWorkingCount) append(":stream=") append(stream?.hashCode() ?: 0) append(":outbox=") @@ -288,8 +327,28 @@ internal fun chatTimelineItemKey(item: ChatTimelineItem): String = is ChatTimelineItem.RecoveryOutboxCommand -> "outbox-recovery:${item.item.id}" is ChatTimelineItem.OutboxRecoveryHeader -> "outbox-recovery-header" is ChatTimelineItem.PendingTools -> "tools" + is ChatTimelineItem.SubagentActivity -> "subagent-activity" is ChatTimelineItem.QuestionPrompt -> "question:${item.prompt.record.id}" is ChatTimelineItem.TurnRecapSummary -> "turn-recap" is ChatTimelineItem.StreamingAssistant -> "stream" ChatTimelineItem.Thinking -> "thinking" } + +internal data class VisibleSubagentActivities( + val activities: List, + val moreWorkingCount: Int, +) + +internal fun visibleSubagentActivities(activities: Collection): VisibleSubagentActivities { + val working = activities.filter(ChatSubagentActivity::isWorking).sortedWith(compareBy { it.startedAtMs }.thenBy { it.id }) + val finished = + activities + .filterNot(ChatSubagentActivity::isWorking) + .sortedWith(compareByDescending { it.endedAtMs ?: Long.MIN_VALUE }.thenBy { it.id }) + val visible = (working + finished).take(5) + return VisibleSubagentActivities( + activities = visible, + moreWorkingCount = + working.count { it.status == "running" && it !in visible }, + ) +} diff --git a/apps/android/app/src/test/java/ai/openclaw/app/chat/BackgroundTaskTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/chat/BackgroundTaskTest.kt index d48dda7e465a..8e1ed98c3578 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/chat/BackgroundTaskTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/chat/BackgroundTaskTest.kt @@ -46,6 +46,17 @@ class BackgroundTaskTest { assertEquals(BackgroundTaskDisplayStatus.Running, tasks.single().displayStatus) } + @Test + fun runningTaskPrefersLiveActivityOverProgressSummary() { + val task = + parseBackgroundTasks( + json, + """{"tasks":[{"id":"task-activity","status":"running","runtime":"subagent","lastActivity":"Editing timeline rows","progressSummary":"Initial milestone"}]}""", + ).single() + + assertEquals("Editing timeline rows", task.output) + } + @Test fun listsActiveAndRecentTasksWithoutRequestingPrompts() = runTest { diff --git a/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerStreamReplayTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerStreamReplayTest.kt index caf165d14a2a..b9446125a7ad 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerStreamReplayTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerStreamReplayTest.kt @@ -299,6 +299,49 @@ class ChatControllerStreamReplayTest { assertEquals(2, controller.messages.value.size) } + @Test + fun liveEditDiffCreatesAndUpdatesPendingToolUntilResult() = + runTest { + val gateway = ScriptedGateway(json) + gateway.respondChatSend(status = "started") + val controller = newController(gateway) + controller.handleGatewayEvent("health", null) + assertTrue(controller.sendMessageAwaitAcceptance("edit the file", "off", emptyList())) + val runId = requireNotNull(gateway.lastRunId) + + controller.handleGatewayEvent( + "agent", + """{"sessionKey":"main","runId":"$runId","ts":10,"stream":"tool","data":{"phase":"input_delta","name":"edit","toolCallId":"tool-1","diff":{"added":4,"removed":1}}}""", + ) + assertEquals( + ChatDiffStat(added = 4, removed = 1), + controller.pendingToolCalls.value + .single() + .liveDiff, + ) + + controller.handleGatewayEvent( + "agent", + """{"sessionKey":"main","runId":"$runId","ts":11,"stream":"tool","data":{"phase":"input_delta","name":"edit","toolCallId":"tool-1","diff":{"added":8,"removed":2}}}""", + ) + controller.handleGatewayEvent( + "agent", + """{"sessionKey":"main","runId":"$runId","ts":12,"stream":"tool","data":{"phase":"start","name":"edit","toolCallId":"tool-1","args":{"path":"a.kt"}}}""", + ) + assertEquals( + ChatDiffStat(added = 8, removed = 2), + controller.pendingToolCalls.value + .single() + .liveDiff, + ) + + controller.handleGatewayEvent( + "agent", + """{"sessionKey":"main","runId":"$runId","ts":13,"stream":"tool","data":{"phase":"result","name":"edit","toolCallId":"tool-1"}}""", + ) + assertTrue(controller.pendingToolCalls.value.isEmpty()) + } + @Test @OptIn(ExperimentalCoroutinesApi::class) fun staleHistoryResponseIsDroppedByGenerationTracking() = diff --git a/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerSubagentActivityTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerSubagentActivityTest.kt new file mode 100644 index 000000000000..8cb97a1e9bf5 --- /dev/null +++ b/apps/android/app/src/test/java/ai/openclaw/app/chat/ChatControllerSubagentActivityTest.kt @@ -0,0 +1,163 @@ +package ai.openclaw.app.chat + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +class ChatControllerSubagentActivityTest { + private val json = Json { ignoreUnknownKeys = true } + + @Test + fun taskEventsFilterAndFoldRetainedActivity() = + runTest { + val controller = newController() + + controller.handleGatewayEvent("task", taskPayload(id = "wrong-runtime", runtime = "cli")) + controller.handleGatewayEvent("task", taskPayload(id = "wrong-session", sessionKey = "other")) + assertTrue(controller.subagentActivities.value.isEmpty()) + + controller.handleGatewayEvent( + "task", + taskPayload( + id = "task-1", + status = "running", + progressSummary = "Planning changes", + lastToolName = "read", + ), + ) + assertEquals( + "Planning changes", + controller.subagentActivities.value + .getValue("task-1") + .snippet, + ) + + controller.handleGatewayEvent( + "task", + taskPayload( + id = "task-1", + status = "running", + lastActivity = "Editing ChatController", + diffStat = Triple(2, 12, 3), + ), + ) + controller.handleGatewayEvent( + "task", + taskPayload( + id = "task-1", + status = "completed", + terminalSummary = "Implementation complete", + ), + ) + + val finished = controller.subagentActivities.value.getValue("task-1") + assertEquals("completed", finished.status) + assertEquals("Editing ChatController", finished.snippet) + assertEquals(ChatDiffStat(added = 12, removed = 3, files = 2), finished.diffStat) + assertEquals("Implementation complete", finished.terminalSummary) + assertEquals("agent:worker:subagent:task-1", finished.childSessionKey) + } + + @Test + @OptIn(ExperimentalCoroutinesApi::class) + fun terminalActivityExpiresAfterSixtySeconds() = + runTest { + val controller = newController() + controller.handleGatewayEvent("task", taskPayload(id = "task-1", status = "completed")) + + advanceTimeBy(59_999) + runCurrent() + assertTrue("task-1" in controller.subagentActivities.value) + + advanceTimeBy(1) + runCurrent() + assertTrue(controller.subagentActivities.value.isEmpty()) + } + + @Test + fun sessionSwitchClearsActivityButParentRunCleanupDoesNot() = + runTest { + val controller = newController() + controller.handleGatewayEvent("task", taskPayload(id = "task-1", status = "running")) + + controller.onDisconnected("offline") + assertTrue("task-1" in controller.subagentActivities.value) + + controller.switchSession("other") + assertTrue(controller.subagentActivities.value.isEmpty()) + } + + @Test + fun sequenceGapClearsActivityThatCanNoLongerConverge() = + runTest { + val controller = newController() + controller.handleGatewayEvent("task", taskPayload(id = "task-1", status = "running")) + + controller.handleGatewayEvent("seqGap", null) + + assertTrue(controller.subagentActivities.value.isEmpty()) + } + + @Test + fun errorOnlyFailureRetainsTerminalDetail() = + runTest { + val controller = newController() + controller.handleGatewayEvent( + "task", + taskPayload(id = "task-1", status = "failed", error = "Worker could not start"), + ) + + assertEquals( + "Worker could not start", + controller.subagentActivities.value + .getValue("task-1") + .error, + ) + } + + private fun TestScope.newController(): ChatController = + ChatController( + scope = backgroundScope, + json = json, + requestGateway = { _, _ -> "{}" }, + ) + + private fun taskPayload( + id: String, + runtime: String = "subagent", + sessionKey: String = "main", + status: String = "queued", + lastActivity: String? = null, + progressSummary: String? = null, + lastToolName: String? = null, + terminalSummary: String? = null, + error: String? = null, + diffStat: Triple? = null, + ): String = + buildString { + append("{\"action\":\"upserted\",\"task\":{") + append("\"id\":\"").append(id).append("\",") + append("\"runtime\":\"").append(runtime).append("\",") + append("\"sessionKey\":\"").append(sessionKey).append("\",") + append("\"childSessionKey\":\"agent:worker:subagent:").append(id).append("\",") + append("\"status\":\"").append(status).append("\",") + append("\"startedAt\":1000") + lastActivity?.let { append(",\"lastActivity\":\"").append(it).append("\"") } + progressSummary?.let { append(",\"progressSummary\":\"").append(it).append("\"") } + lastToolName?.let { append(",\"lastToolName\":\"").append(it).append("\"") } + terminalSummary?.let { append(",\"terminalSummary\":\"").append(it).append("\"") } + error?.let { append(",\"error\":\"").append(it).append("\"") } + diffStat?.let { (files, added, removed) -> + append(",\"diffStat\":{\"files\":").append(files) + append(",\"added\":").append(added) + append(",\"removed\":").append(removed).append('}') + } + append("}}") + } +} diff --git a/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatTimelineTest.kt b/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatTimelineTest.kt index 5ad37692f063..4456a34694c2 100644 --- a/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatTimelineTest.kt +++ b/apps/android/app/src/test/java/ai/openclaw/app/ui/chat/ChatTimelineTest.kt @@ -1,10 +1,12 @@ package ai.openclaw.app.ui.chat +import ai.openclaw.app.chat.ChatDiffStat import ai.openclaw.app.chat.ChatMessage import ai.openclaw.app.chat.ChatMessageContent import ai.openclaw.app.chat.ChatOutboxItem import ai.openclaw.app.chat.ChatOutboxStatus import ai.openclaw.app.chat.ChatPendingToolCall +import ai.openclaw.app.chat.ChatSubagentActivity import ai.openclaw.app.chat.OUTBOX_OWNER_CHANGED_ERROR import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -291,6 +293,98 @@ class ChatTimelineTest { assertEquals(listOf(mismatched), outboxItemsForRecovery(listOf(mismatched))) } + @Test + fun subagentRowsStayKeyedByTaskAndParticipateInLiveContentVersion() { + val activity = subagentActivity(id = "task-1", snippet = "Reading files", added = 2) + val first = + buildChatTimeline( + messages = emptyList(), + pendingRunCount = 0, + pendingToolCalls = emptyList(), + streamingAssistantText = null, + subagentActivities = mapOf(activity.id to activity), + ) + val updated = activity.copy(snippet = "Editing files", diffStat = ChatDiffStat(added = 8, removed = 3, files = 2)) + val second = + buildChatTimeline( + messages = emptyList(), + pendingRunCount = 0, + pendingToolCalls = emptyList(), + streamingAssistantText = null, + subagentActivities = mapOf(updated.id to updated), + ) + + assertEquals(listOf("subagent-activity"), first.items.map(::chatTimelineItemKey)) + assertEquals(first.items.map(::chatTimelineItemKey), second.items.map(::chatTimelineItemKey)) + assertTrue(first.latestContentVersion != second.latestContentVersion) + } + + @Test + fun subagentRowsCapAtFiveAndOverflowCountsOnlyHiddenWorkingRows() { + val working = (1..6).map { index -> subagentActivity(id = "task-$index", startedAtMs = index.toLong()) } + val queued = subagentActivity(id = "task-queued", status = "queued", startedAtMs = 100) + val finished = subagentActivity(id = "task-finished", status = "completed", startedAtMs = 0, endedAtMs = 20) + + val timeline = + buildChatTimeline( + messages = emptyList(), + pendingRunCount = 0, + pendingToolCalls = listOf(ChatPendingToolCall(toolCallId = "tool-1", name = "edit", startedAtMs = 1)), + streamingAssistantText = null, + subagentActivities = (working + queued + finished).associateBy(ChatSubagentActivity::id), + ) + + assertEquals( + listOf("tools", "subagent-activity"), + timeline.items.map(::chatTimelineItemKey), + ) + val row = timeline.items.filterIsInstance().single() + assertEquals(listOf("task-1", "task-2", "task-3", "task-4", "task-5"), row.activities.map { it.id }) + assertEquals(1, row.moreWorkingCount) + } + + @Test + fun liveToolDiffChangesTimelineContentVersion() { + val pending = ChatPendingToolCall(toolCallId = "tool-1", name = "edit", startedAtMs = 1) + val initial = + buildChatTimeline( + messages = emptyList(), + pendingRunCount = 0, + pendingToolCalls = listOf(pending), + streamingAssistantText = null, + ) + val updated = + buildChatTimeline( + messages = emptyList(), + pendingRunCount = 0, + pendingToolCalls = listOf(pending.copy(liveDiff = ChatDiffStat(added = 4, removed = 1))), + streamingAssistantText = null, + ) + + assertEquals(initial.items.map(::chatTimelineItemKey), updated.items.map(::chatTimelineItemKey)) + assertTrue(initial.latestContentVersion != updated.latestContentVersion) + } + + private fun subagentActivity( + id: String, + status: String = "running", + snippet: String? = null, + added: Int = 0, + startedAtMs: Long = 1, + endedAtMs: Long? = null, + ): ChatSubagentActivity = + ChatSubagentActivity( + id = id, + status = status, + snippet = snippet, + diffStat = ChatDiffStat(added = added, removed = 0, files = 1), + terminalSummary = null, + error = null, + startedAtMs = startedAtMs, + endedAtMs = endedAtMs, + childSessionKey = "agent:worker:subagent:$id", + ) + private fun textMessage( id: String, role: String, diff --git a/scripts/protocol-event-coverage.allowlist.json b/scripts/protocol-event-coverage.allowlist.json index 389a051cc7b6..0f11f43203c0 100644 --- a/scripts/protocol-event-coverage.allowlist.json +++ b/scripts/protocol-event-coverage.allowlist.json @@ -55,7 +55,6 @@ "shutdown": "Android relies on socket close plus reconnect/backoff instead of the shutdown notice.", "skills.changed": "Skills settings is a macOS operator surface; Android does not expose skill management yet.", "talk.mode": "Android toggles talk mode locally; gateway talk.mode sync is not consumed.", - "task": "Background task activity is not surfaced in the Android app.", "task.suggestion": "Task suggestion cards are a Control UI-only surface; Android does not render them.", "terminal.data": "Embedded terminal is a web/desktop surface; Android has no terminal client.", "terminal.exit": "Embedded terminal is a web/desktop surface; Android has no terminal client.",