diff --git a/apps/.i18n/native-source.json b/apps/.i18n/native-source.json index bc43f4473e85..e2289f5ec765 100644 --- a/apps/.i18n/native-source.json +++ b/apps/.i18n/native-source.json @@ -26347,7 +26347,7 @@ }, { "kind": "ui-localized-call", - "line": 150, + "line": 159, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Caught up", "surface": "apple", @@ -26355,7 +26355,7 @@ }, { "kind": "conditional-branch", - "line": 153, + "line": 162, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "No chats or approvals need you", "surface": "apple", @@ -26363,7 +26363,7 @@ }, { "kind": "conditional-branch", - "line": 154, + "line": 163, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Waiting for iPhone sync", "surface": "apple", @@ -26371,7 +26371,7 @@ }, { "kind": "ui-named-argument", - "line": 161, + "line": 170, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Continue on iPhone", "surface": "apple", @@ -26379,7 +26379,7 @@ }, { "kind": "ui-localized-call", - "line": 202, + "line": 211, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Latest", "surface": "apple", @@ -26387,7 +26387,7 @@ }, { "kind": "ui-localized-call", - "line": 238, + "line": 247, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval waiting", "surface": "apple", @@ -26395,7 +26395,7 @@ }, { "kind": "ui-localized-call", - "line": 244, + "line": 253, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Latest update", "surface": "apple", @@ -26403,7 +26403,7 @@ }, { "kind": "ui-localized-call", - "line": 247, + "line": 256, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Nothing waiting", "surface": "apple", @@ -26411,7 +26411,7 @@ }, { "kind": "ui-localized-call", - "line": 263, + "line": 272, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval needed", "surface": "apple", @@ -26419,7 +26419,7 @@ }, { "kind": "ui-modifier", - "line": 286, + "line": 295, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Opens the full command before decisions are available", "surface": "apple", @@ -26427,7 +26427,7 @@ }, { "kind": "ui-localized-call", - "line": 290, + "line": 299, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Loading", "surface": "apple", @@ -26435,7 +26435,7 @@ }, { "kind": "ui-localized-call", - "line": 291, + "line": 300, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Loading approval", "surface": "apple", @@ -26443,7 +26443,7 @@ }, { "kind": "ui-localized-call", - "line": 294, + "line": 303, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Waiting for your iPhone", "surface": "apple", @@ -26451,7 +26451,7 @@ }, { "kind": "ui-localized-call", - "line": 295, + "line": 304, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Syncing", "surface": "apple", @@ -26459,7 +26459,7 @@ }, { "kind": "ui-localized-call", - "line": 298, + "line": 307, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Unavailable", "surface": "apple", @@ -26467,7 +26467,7 @@ }, { "kind": "ui-localized-call", - "line": 299, + "line": 308, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval not loaded", "surface": "apple", @@ -26475,7 +26475,7 @@ }, { "kind": "ui-localized-call", - "line": 302, + "line": 311, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval details have not loaded", "surface": "apple", @@ -26483,7 +26483,7 @@ }, { "kind": "ui-localized-call", - "line": 303, + "line": 312, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Retry", "surface": "apple", @@ -26491,7 +26491,7 @@ }, { "kind": "ui-named-argument", - "line": 305, + "line": 314, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Review again", "surface": "apple", @@ -26499,7 +26499,7 @@ }, { "kind": "ui-named-argument", - "line": 322, + "line": 331, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Open all approvals", "surface": "apple", @@ -26507,7 +26507,7 @@ }, { "kind": "ui-localized-call", - "line": 333, + "line": 342, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Connection", "surface": "apple", @@ -26515,7 +26515,7 @@ }, { "kind": "ui-localized-call", - "line": 335, + "line": 344, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Watch node online", "surface": "apple", @@ -26523,7 +26523,7 @@ }, { "kind": "ui-localized-call", - "line": 336, + "line": 345, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Direct Gateway", "surface": "apple", @@ -26531,7 +26531,7 @@ }, { "kind": "ui-localized-call", - "line": 343, + "line": 352, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Direct", "surface": "apple", @@ -26539,7 +26539,7 @@ }, { "kind": "ui-localized-call", - "line": 344, + "line": 353, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Setup", "surface": "apple", @@ -26547,7 +26547,7 @@ }, { "kind": "ui-localized-call", - "line": 346, + "line": 355, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Enable from iPhone", "surface": "apple", @@ -26555,7 +26555,7 @@ }, { "kind": "conditional-branch", - "line": 349, + "line": 358, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Uses Wi-Fi or cellular while OpenClaw is active", "surface": "apple", @@ -26563,7 +26563,7 @@ }, { "kind": "conditional-branch", - "line": 350, + "line": 359, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Open iPhone Settings → Apple Watch", "surface": "apple", @@ -26571,7 +26571,7 @@ }, { "kind": "ui-localized-call", - "line": 352, + "line": 361, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Online", "surface": "apple", @@ -26579,7 +26579,7 @@ }, { "kind": "ui-localized-call", - "line": 353, + "line": 362, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Offline", "surface": "apple", @@ -26587,7 +26587,7 @@ }, { "kind": "ui-localized-call-multiline", - "line": 356, + "line": 365, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Direct mode supports device info, status, and notifications. Chat, Talk, and approvals still use the iPhone.", "surface": "apple", @@ -26595,7 +26595,7 @@ }, { "kind": "ui-call", - "line": 366, + "line": 375, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Direct connection", "surface": "apple", @@ -26603,7 +26603,7 @@ }, { "kind": "ui-named-argument", - "line": 372, + "line": 381, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Forget direct setup", "surface": "apple", @@ -26611,7 +26611,7 @@ }, { "kind": "ui-localized-call", - "line": 378, + "line": 387, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "The iPhone securely sends a one-time setup code. Existing relay features stay available.", "surface": "apple", @@ -26619,7 +26619,7 @@ }, { "kind": "ui-localized-call", - "line": 430, + "line": 439, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "AI agent online", "surface": "apple", @@ -26627,7 +26627,7 @@ }, { "kind": "ui-localized-call", - "line": 431, + "line": 440, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Reconnect on iPhone", "surface": "apple", @@ -26635,7 +26635,7 @@ }, { "kind": "ui-localized-call", - "line": 433, + "line": 442, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Pair iPhone", "surface": "apple", @@ -26643,7 +26643,7 @@ }, { "kind": "ui-localized-call", - "line": 437, + "line": 446, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Next up", "surface": "apple", @@ -26651,7 +26651,7 @@ }, { "kind": "ui-localized-call", - "line": 439, + "line": 448, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Running", "surface": "apple", @@ -26659,7 +26659,7 @@ }, { "kind": "ui-localized-call", - "line": 440, + "line": 449, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Pairing", "surface": "apple", @@ -26667,7 +26667,7 @@ }, { "kind": "ui-localized-call", - "line": 455, + "line": 464, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval waiting on your wrist", "surface": "apple", @@ -26675,7 +26675,7 @@ }, { "kind": "ui-localized-call", - "line": 461, + "line": 470, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Ready for quick actions", "surface": "apple", @@ -26683,7 +26683,7 @@ }, { "kind": "ui-localized-call", - "line": 468, + "line": 477, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "^[\\(count) approval](inflect: true) waiting", "surface": "apple", @@ -26691,7 +26691,7 @@ }, { "kind": "ui-localized-call", - "line": 480, + "line": 489, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Decide from watch", "surface": "apple", @@ -26699,7 +26699,7 @@ }, { "kind": "ui-localized-call", - "line": 481, + "line": 490, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "No approvals", "surface": "apple", @@ -26707,7 +26707,7 @@ }, { "kind": "ui-localized-call", - "line": 489, + "line": 498, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Expires in %@", "surface": "apple", @@ -26715,7 +26715,7 @@ }, { "kind": "ui-localized-call", - "line": 496, + "line": 505, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Review before it runs", "surface": "apple", @@ -26723,7 +26723,7 @@ }, { "kind": "ui-localized-call", - "line": 503, + "line": 512, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Sending", "surface": "apple", @@ -26731,7 +26731,7 @@ }, { "kind": "ui-localized-call", - "line": 552, + "line": 561, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Good morning", "surface": "apple", @@ -26739,7 +26739,7 @@ }, { "kind": "ui-localized-call", - "line": 553, + "line": 562, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Good afternoon", "surface": "apple", @@ -26747,7 +26747,7 @@ }, { "kind": "ui-localized-call", - "line": 554, + "line": 563, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Good evening", "surface": "apple", @@ -26755,7 +26755,7 @@ }, { "kind": "ui-localized-call", - "line": 568, + "line": 577, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Synced", "surface": "apple", @@ -26763,7 +26763,7 @@ }, { "kind": "ui-localized-call", - "line": 569, + "line": 578, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Waiting for iPhone", "surface": "apple", @@ -26771,7 +26771,7 @@ }, { "kind": "ui-localized-call", - "line": 573, + "line": 582, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Just now", "surface": "apple", @@ -26779,7 +26779,7 @@ }, { "kind": "ui-localized-call", - "line": 591, + "line": 600, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Requires confirmation", "surface": "apple", @@ -26787,7 +26787,7 @@ }, { "kind": "ui-localized-call", - "line": 593, + "line": 602, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Dismiss this update", "surface": "apple", @@ -26795,7 +26795,7 @@ }, { "kind": "ui-localized-call", - "line": 595, + "line": 604, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Send from watch", "surface": "apple", @@ -26803,7 +26803,7 @@ }, { "kind": "ui-localized-call", - "line": 603, + "line": 612, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "less than 1 min", "surface": "apple", @@ -26811,7 +26811,7 @@ }, { "kind": "ui-localized-call", - "line": 606, + "line": 615, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "%@ min", "surface": "apple", @@ -26819,7 +26819,7 @@ }, { "kind": "ui-named-argument", - "line": 836, + "line": 845, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Inbox", "surface": "apple", @@ -26827,7 +26827,7 @@ }, { "kind": "ui-call", - "line": 1072, + "line": 1081, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Command", "surface": "apple", @@ -26835,7 +26835,7 @@ }, { "kind": "ui-modifier", - "line": 1091, + "line": 1100, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Command to review", "surface": "apple", @@ -26843,7 +26843,7 @@ }, { "kind": "ui-localized-call", - "line": 1108, + "line": 1117, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Sending decision...", "surface": "apple", @@ -26851,7 +26851,7 @@ }, { "kind": "ui-localized-call", - "line": 1162, + "line": 1171, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "System", "surface": "apple", @@ -26859,7 +26859,7 @@ }, { "kind": "conditional-branch", - "line": 1164, + "line": 1173, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "OpenClaw", "surface": "apple", @@ -26867,7 +26867,7 @@ }, { "kind": "ui-named-argument", - "line": 1214, + "line": 1223, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Refresh", "surface": "apple", @@ -26875,7 +26875,7 @@ }, { "kind": "ui-modifier", - "line": 1244, + "line": 1253, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Chat", "surface": "apple", @@ -26883,7 +26883,7 @@ }, { "kind": "ui-localized-call", - "line": 1262, + "line": 1271, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Speaking reply…", "surface": "apple", @@ -26891,7 +26891,7 @@ }, { "kind": "ui-localized-call", - "line": 1265, + "line": 1274, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Waiting for spoken reply…", "surface": "apple", @@ -26899,7 +26899,7 @@ }, { "kind": "ui-call", - "line": 1309, + "line": 1318, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "No chat synced", "surface": "apple", @@ -26907,7 +26907,7 @@ }, { "kind": "ui-call", - "line": 1316, + "line": 1325, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Tap the message pill below to start from your watch.", "surface": "apple", @@ -26915,7 +26915,7 @@ }, { "kind": "ui-call", - "line": 1337, + "line": 1346, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "You", "surface": "apple", @@ -26923,7 +26923,7 @@ }, { "kind": "ui-call", - "line": 1364, + "line": 1373, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Message OpenClaw", "surface": "apple", @@ -26931,7 +26931,7 @@ }, { "kind": "ui-localized-call", - "line": 1422, + "line": 1431, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Stop speaking", "surface": "apple", @@ -26939,7 +26939,7 @@ }, { "kind": "ui-localized-call", - "line": 1425, + "line": 1434, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Cancel voice turn", "surface": "apple", @@ -26947,7 +26947,7 @@ }, { "kind": "ui-localized-call", - "line": 1427, + "line": 1436, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Start voice turn", "surface": "apple", @@ -26955,7 +26955,7 @@ }, { "kind": "ui-named-argument", - "line": 1467, + "line": 1476, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approvals", "surface": "apple", @@ -26963,7 +26963,7 @@ }, { "kind": "ui-localized-call", - "line": 1470, + "line": 1479, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Clear", "surface": "apple", @@ -26971,7 +26971,7 @@ }, { "kind": "ui-localized-call", - "line": 1471, + "line": 1480, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "No approvals waiting", "surface": "apple", @@ -26979,7 +26979,7 @@ }, { "kind": "ui-localized-call", - "line": 1474, + "line": 1483, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "You are caught up", "surface": "apple", @@ -26987,7 +26987,7 @@ }, { "kind": "ui-localized-call", - "line": 1475, + "line": 1484, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Ready", "surface": "apple", @@ -26995,7 +26995,7 @@ }, { "kind": "ui-localized-call", - "line": 1485, + "line": 1494, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Approval", "surface": "apple", @@ -27003,7 +27003,7 @@ }, { "kind": "ui-localized-call", - "line": 1514, + "line": 1523, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Pending review", "surface": "apple", @@ -27011,7 +27011,7 @@ }, { "kind": "ui-localized-call", - "line": 1521, + "line": 1530, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Expires in less than 1 min", "surface": "apple", @@ -27019,7 +27019,7 @@ }, { "kind": "ui-localized-call", - "line": 1524, + "line": 1533, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Expires in %@ min", "surface": "apple", @@ -27027,7 +27027,7 @@ }, { "kind": "ui-named-argument", - "line": 1535, + "line": 1544, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Review Command", "surface": "apple", @@ -27035,7 +27035,7 @@ }, { "kind": "ui-localized-call", - "line": 1539, + "line": 1548, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Review", "surface": "apple", @@ -27043,7 +27043,7 @@ }, { "kind": "ui-localized-call", - "line": 1540, + "line": 1549, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Command execution", "surface": "apple", @@ -27051,7 +27051,7 @@ }, { "kind": "ui-localized-call", - "line": 1545, + "line": 1554, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Now", "surface": "apple", @@ -27059,7 +27059,7 @@ }, { "kind": "ui-named-argument", - "line": 1563, + "line": 1572, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Allow Once", "surface": "apple", @@ -27067,7 +27067,7 @@ }, { "kind": "ui-named-argument", - "line": 1572, + "line": 1581, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Deny", "surface": "apple", @@ -27075,7 +27075,7 @@ }, { "kind": "ui-localized-call", - "line": 1616, + "line": 1625, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Review command below", "surface": "apple", @@ -27083,7 +27083,7 @@ }, { "kind": "ui-localized-call", - "line": 1623, + "line": 1632, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "High risk", "surface": "apple", @@ -27091,7 +27091,7 @@ }, { "kind": "ui-localized-call", - "line": 1625, + "line": 1634, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Medium risk", "surface": "apple", @@ -27099,7 +27099,7 @@ }, { "kind": "ui-localized-call", - "line": 1627, + "line": 1636, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "Low risk", "surface": "apple", @@ -27107,7 +27107,7 @@ }, { "kind": "ui-localized-call", - "line": 1637, + "line": 1646, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "less than 1 minute", "surface": "apple", @@ -27115,7 +27115,7 @@ }, { "kind": "ui-localized-call", - "line": 1641, + "line": 1650, "path": "apps/ios/WatchApp/Sources/WatchInboxView.swift", "source": "^[\\(minutes) minute](inflect: true)", "surface": "apple", @@ -36915,7 +36915,7 @@ }, { "kind": "ui-call", - "line": 588, + "line": 592, "path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift", "source": "z", "surface": "apple", diff --git a/apps/ios/Tests/Logic/WatchMascotTests.swift b/apps/ios/Tests/Logic/WatchMascotTests.swift new file mode 100644 index 000000000000..d55940e402e7 --- /dev/null +++ b/apps/ios/Tests/Logic/WatchMascotTests.swift @@ -0,0 +1,42 @@ +import Testing + +struct WatchMascotTests { + @Test func `approvals are attentive`() { + #expect(watchInboxMascotMood( + hasSnapshot: true, + hasApprovals: true, + hasChats: false) == .attentive) + } + + @Test func `missing snapshot is thinking`() { + #expect(watchInboxMascotMood( + hasSnapshot: false, + hasApprovals: false, + hasChats: false) == .thinking) + } + + @Test func `empty synchronized inbox is sleepy`() { + #expect(watchInboxMascotMood( + hasSnapshot: true, + hasApprovals: false, + hasChats: false) == .sleepy) + } + + @Test func `synchronized chats are idle`() { + #expect(watchInboxMascotMood( + hasSnapshot: true, + hasApprovals: false, + hasChats: true) == .idle) + } + + @Test func `approval and snapshot precedence wins over chats`() { + #expect(watchInboxMascotMood( + hasSnapshot: false, + hasApprovals: true, + hasChats: true) == .attentive) + #expect(watchInboxMascotMood( + hasSnapshot: false, + hasApprovals: false, + hasChats: true) == .thinking) + } +} diff --git a/apps/ios/WatchApp/Sources/WatchInboxView.swift b/apps/ios/WatchApp/Sources/WatchInboxView.swift index 0e34718a3347..8b9cfec55cb0 100644 --- a/apps/ios/WatchApp/Sources/WatchInboxView.swift +++ b/apps/ios/WatchApp/Sources/WatchInboxView.swift @@ -145,6 +145,15 @@ private struct WatchControlSurfaceView: View { } self.inboxPromptBlock } else { + // The decorative mascot belongs only to the empty or waiting inbox surface. + WatchMascot( + mood: watchInboxMascotMood( + hasSnapshot: self.store.hasAppSnapshot, + hasApprovals: self.approvalCount > 0, + hasChats: self.chatCount > 0), + size: 72) + .frame(maxWidth: .infinity) + WatchHeroCard( label: .localized("Clear"), title: .localized("Caught up"), diff --git a/apps/ios/WatchApp/Sources/WatchMascot.swift b/apps/ios/WatchApp/Sources/WatchMascot.swift new file mode 100644 index 000000000000..ba449e23f153 --- /dev/null +++ b/apps/ios/WatchApp/Sources/WatchMascot.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct WatchMascot: View { + @Environment(\.colorScheme) private var colorScheme + @Environment(\.isLuminanceReduced) private var isLuminanceReduced + + let mood: OpenClawMascotMood + let size: CGFloat + + var body: some View { + Group { + if self.isLuminanceReduced { + // Always-On display must stay static so the animation loop never consumes background power. + OpenClawMascotCanvas( + pose: .staticPose(for: self.mood), + palette: .forScheme(self.colorScheme)) + } else { + // Fifteen frames per second keeps the tiny watch animation expressive without needless battery cost. + OpenClawMascotView( + mood: self.mood, + minimumFrameInterval: 1.0 / 15.0) + } + } + .frame(width: self.size, height: self.size) + .accessibilityHidden(true) + } +} + +func watchInboxMascotMood( + hasSnapshot: Bool, + hasApprovals: Bool, + hasChats: Bool) -> OpenClawMascotMood +{ + // Approval decisions demand attention above every other inbox state. + if hasApprovals { return .attentive } + // No snapshot means the watch is still waiting and thinking. + if !hasSnapshot { return .thinking } + // A synchronized inbox with nothing waiting can settle into sleep. + if !hasChats { return .sleepy } + // Existing chats keep the mascot present but neutral. + return .idle +} diff --git a/apps/ios/project.yml b/apps/ios/project.yml index b75dc159994b..d89a208496ce 100644 --- a/apps/ios/project.yml +++ b/apps/ios/project.yml @@ -317,6 +317,15 @@ targets: # Universal talk waveform, compiled directly to keep the watch styling local. - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/TalkWaveformView.swift group: Shared + # Universal mascot, compiled directly to keep the watch styling local. + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotAnimator.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotCanvas+Accessories.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTypography.swift + group: Shared - path: Sources/Fonts buildPhase: resources - path: Resources/Localizable.xcstrings @@ -428,6 +437,16 @@ targets: group: WatchApp/Sources - path: WatchApp/Sources/WatchInboxMessages.swift group: WatchApp/Sources + - path: WatchApp/Sources/WatchMascot.swift + group: WatchApp/Sources + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotAnimator.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotCanvas+Accessories.swift + group: Shared + - path: ../shared/OpenClawKit/Sources/OpenClawChatUI/ChatTypography.swift + group: Shared - path: Sources/Services/ExactOpaqueIdentifier.swift group: Sources/Services dependencies: diff --git a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift index 6b6b667e4ccb..d7fd69583541 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawChatUI/OpenClawMascotView.swift @@ -24,6 +24,7 @@ public struct OpenClawMascotView: View { private let mood: OpenClawMascotMood private let accessory: OpenClawMascotAccessory private let interactive: Bool + private let minimumFrameInterval: TimeInterval private var staticPose: OpenClawMascotPose { var pose = OpenClawMascotPose.staticPose(for: self.mood) @@ -44,16 +45,19 @@ public struct OpenClawMascotView: View { /// - interactive: enables click reactions and auto-sleep (waking takes /// a click). Off by default so the mascot never swallows taps meant /// for an enclosing control. + /// - minimumFrameInterval: minimum redraw interval for animated poses. public init( floats: Bool = true, mood: OpenClawMascotMood = .idle, accessory: OpenClawMascotAccessory = .none, - interactive: Bool = false) + interactive: Bool = false, + minimumFrameInterval: TimeInterval = 1.0 / 30.0) { self.floats = floats self.mood = mood self.accessory = accessory self.interactive = interactive + self.minimumFrameInterval = minimumFrameInterval self._animator = State(initialValue: OpenClawMascotAnimator(allowsAutoSleep: interactive)) } @@ -68,7 +72,7 @@ public struct OpenClawMascotView: View { @ViewBuilder private func animatedMascot(palette: OpenClawMascotPalette) -> some View { - let core = TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in + let core = TimelineView(.animation(minimumInterval: self.minimumFrameInterval)) { timeline in let pose = self.animator.pose(at: timeline.date.timeIntervalSinceReferenceDate) // Float translates the whole canvas like the site floats the hero // container; drawing the offset inside the canvas would clip the