mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(apple): show live subagent activity in chat (#121815)
* feat(apple): show live subagent activity in chat * chore(apple): refresh native localization inventory
This commit is contained in:
committed by
GitHub
parent
d7ea18e600
commit
4b46a14a54
@@ -19083,7 +19083,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 45,
|
"line": 46,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Background task",
|
"source": "Background task",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19091,7 +19091,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 54,
|
"line": 55,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Queued",
|
"source": "Queued",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19099,7 +19099,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 56,
|
"line": 57,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Completed",
|
"source": "Completed",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19107,7 +19107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 57,
|
"line": 58,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Failed",
|
"source": "Failed",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19115,7 +19115,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 63,
|
"line": 64,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Subagent",
|
"source": "Subagent",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19123,7 +19123,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 64,
|
"line": 65,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Cron",
|
"source": "Cron",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19131,7 +19131,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 67,
|
"line": 68,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Task",
|
"source": "Task",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19139,7 +19139,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 169,
|
"line": 170,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Loading background tasks…",
|
"source": "Loading background tasks…",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19147,7 +19147,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 181,
|
"line": 182,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Tasks for this agent will appear here.",
|
"source": "Tasks for this agent will appear here.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19155,7 +19155,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 192,
|
"line": 193,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "No running tasks",
|
"source": "No running tasks",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19163,7 +19163,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 201,
|
"line": 202,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Running",
|
"source": "Running",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19171,7 +19171,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 205,
|
"line": 206,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "No finished tasks",
|
"source": "No finished tasks",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19179,7 +19179,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 214,
|
"line": 215,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Finished",
|
"source": "Finished",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19187,7 +19187,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-modifier",
|
"kind": "ui-modifier",
|
||||||
"line": 221,
|
"line": 222,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Background Tasks",
|
"source": "Background Tasks",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19195,7 +19195,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 229,
|
"line": 230,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Refresh",
|
"source": "Refresh",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19203,7 +19203,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 336,
|
"line": 337,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Prompt",
|
"source": "Prompt",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19211,7 +19211,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 338,
|
"line": 339,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Loading…",
|
"source": "Loading…",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19219,7 +19219,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 339,
|
"line": 340,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Prompt unavailable.",
|
"source": "Prompt unavailable.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19227,7 +19227,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 341,
|
"line": 342,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Output",
|
"source": "Output",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19235,7 +19235,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 342,
|
"line": 343,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "No output yet.",
|
"source": "No output yet.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -19243,7 +19243,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-modifier",
|
"kind": "ui-modifier",
|
||||||
"line": 352,
|
"line": 353,
|
||||||
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
"path": "apps/ios/Sources/Design/BackgroundTasksScreen.swift",
|
||||||
"source": "Task Details",
|
"source": "Task Details",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39491,7 +39491,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 936,
|
"line": 943,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Talk mode uses the primary Gateway window",
|
"source": "Talk mode uses the primary Gateway window",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39499,7 +39499,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 938,
|
"line": 945,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Talk mode off",
|
"source": "Talk mode off",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39507,7 +39507,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 940,
|
"line": 947,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Talk mode paused",
|
"source": "Talk mode paused",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39515,7 +39515,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 943,
|
"line": 950,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Talk mode ready",
|
"source": "Talk mode ready",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39523,7 +39523,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 944,
|
"line": 951,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Listening",
|
"source": "Listening",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39531,7 +39531,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 945,
|
"line": 952,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Thinking",
|
"source": "Thinking",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39539,7 +39539,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 946,
|
"line": 953,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Speaking",
|
"source": "Speaking",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39547,7 +39547,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 950,
|
"line": 957,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "What would you like to work on?",
|
"source": "What would you like to work on?",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39555,7 +39555,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 954,
|
"line": 961,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Check OpenClaw status",
|
"source": "Check OpenClaw status",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39563,7 +39563,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 955,
|
"line": 962,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Summarize the current OpenClaw status and tell me what needs attention.",
|
"source": "Summarize the current OpenClaw status and tell me what needs attention.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39571,7 +39571,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 958,
|
"line": 965,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "What can you do?",
|
"source": "What can you do?",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39579,7 +39579,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 959,
|
"line": 966,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Show me what you can help with on this Mac right now.",
|
"source": "Show me what you can help with on this Mac right now.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39587,7 +39587,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 962,
|
"line": 969,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Catch me up",
|
"source": "Catch me up",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -39595,7 +39595,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 963,
|
"line": 970,
|
||||||
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
"path": "apps/macos/Sources/OpenClaw/WebChatSwiftUI.swift",
|
||||||
"source": "Summarize what happened in my threads since yesterday.",
|
"source": "Summarize what happened in my threads since yesterday.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40499,7 +40499,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "conditional-branch",
|
"kind": "conditional-branch",
|
||||||
"line": 701,
|
"line": 702,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Voice note",
|
"source": "Voice note",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40507,7 +40507,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 742,
|
"line": 743,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Attachment",
|
"source": "Attachment",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40515,7 +40515,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 782,
|
"line": 783,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Writing",
|
"source": "Writing",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40523,7 +40523,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 821,
|
"line": 822,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Preparing audio…",
|
"source": "Preparing audio…",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40531,7 +40531,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 824,
|
"line": 825,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Speaking…",
|
"source": "Speaking…",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40539,7 +40539,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "conditional-branch",
|
"kind": "conditional-branch",
|
||||||
"line": 832,
|
"line": 833,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Preparing audio, tap to cancel",
|
"source": "Preparing audio, tap to cancel",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -40547,7 +40547,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "conditional-branch",
|
"kind": "conditional-branch",
|
||||||
"line": 833,
|
"line": 834,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMessageViews.swift",
|
||||||
"source": "Speaking, tap to stop",
|
"source": "Speaking, tap to stop",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41617,6 +41617,14 @@
|
|||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
"id": "native.apple.b09b91392835fe58"
|
"id": "native.apple.b09b91392835fe58"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ui-localized-call",
|
||||||
|
"line": 14,
|
||||||
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatSubagentActivityViews.swift",
|
||||||
|
"source": "+%1$lld more working",
|
||||||
|
"surface": "apple",
|
||||||
|
"id": "native.apple.d5a544f5ba642c60"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 49,
|
"line": 49,
|
||||||
@@ -41643,7 +41651,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 83,
|
"line": 86,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
||||||
"source": "Running",
|
"source": "Running",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41651,7 +41659,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "conditional-branch",
|
"kind": "conditional-branch",
|
||||||
"line": 133,
|
"line": 140,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
||||||
"source": "Collapse tool result",
|
"source": "Collapse tool result",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41659,7 +41667,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "conditional-branch",
|
"kind": "conditional-branch",
|
||||||
"line": 133,
|
"line": 140,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
||||||
"source": "Expand tool result",
|
"source": "Expand tool result",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41667,7 +41675,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 169,
|
"line": 176,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
||||||
"source": "Show less",
|
"source": "Show less",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41675,7 +41683,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 171,
|
"line": 178,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatToolActivityViews.swift",
|
||||||
"source": "Show all %lld lines",
|
"source": "Show all %lld lines",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41731,7 +41739,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 585,
|
"line": 592,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Retry Send",
|
"source": "Retry Send",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41739,7 +41747,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 599,
|
"line": 606,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Delete",
|
"source": "Delete",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41747,7 +41755,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 635,
|
"line": 642,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Stop Listening",
|
"source": "Stop Listening",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41755,7 +41763,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 638,
|
"line": 645,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Listen",
|
"source": "Listen",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41763,7 +41771,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-modifier",
|
"kind": "ui-modifier",
|
||||||
"line": 735,
|
"line": 742,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Jump to latest reply",
|
"source": "Jump to latest reply",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41771,7 +41779,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-named-argument",
|
"kind": "ui-named-argument",
|
||||||
"line": 755,
|
"line": 762,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Refresh",
|
"source": "Refresh",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41779,7 +41787,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 1165,
|
"line": 1175,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Copy Message",
|
"source": "Copy Message",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41787,7 +41795,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 1188,
|
"line": 1198,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Open Full Message",
|
"source": "Open Full Message",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41795,7 +41803,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 1207,
|
"line": 1217,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Rewind to Here",
|
"source": "Rewind to Here",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41803,7 +41811,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 1227,
|
"line": 1237,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Fork from Here",
|
"source": "Fork from Here",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41811,7 +41819,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 1253,
|
"line": 1263,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Reply",
|
"source": "Reply",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41819,7 +41827,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 1263,
|
"line": 1273,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "You",
|
"source": "You",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41827,7 +41835,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 1265,
|
"line": 1275,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Assistant",
|
"source": "Assistant",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41835,7 +41843,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-call",
|
"kind": "ui-call",
|
||||||
"line": 1331,
|
"line": 1341,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Loading chat",
|
"source": "Loading chat",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41843,7 +41851,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-modifier",
|
"kind": "ui-modifier",
|
||||||
"line": 1411,
|
"line": 1421,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatView.swift",
|
||||||
"source": "Dismiss",
|
"source": "Dismiss",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
@@ -41971,7 +41979,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"kind": "ui-localized-call",
|
"kind": "ui-localized-call",
|
||||||
"line": 1225,
|
"line": 1205,
|
||||||
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
|
"path": "apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatViewModel.swift",
|
||||||
"source": "Remove attachments or wait for delivery to resolve before switching chats.",
|
"source": "Remove attachments or wait for delivery to resolve before switching chats.",
|
||||||
"surface": "apple",
|
"surface": "apple",
|
||||||
|
|||||||
@@ -775,6 +775,13 @@ struct IOSGatewayChatTransport: OpenClawChatTransport {
|
|||||||
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listTasks(sessionKey: String, agentID: String?) async throws -> [TaskSummary] {
|
||||||
|
let data = try await gateway.request(OpenClawChatGatewayRequests.tasksList(
|
||||||
|
sessionKey: sessionKey,
|
||||||
|
agentID: agentID))
|
||||||
|
return try JSONDecoder().decode(TasksListResult.self, from: data).tasks
|
||||||
|
}
|
||||||
|
|
||||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||||
let data = try await gateway.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
let data = try await gateway.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
||||||
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ struct MobileBackgroundTask: Decodable, Identifiable, Equatable {
|
|||||||
let updatedAt: Timestamp?
|
let updatedAt: Timestamp?
|
||||||
let startedAt: Timestamp?
|
let startedAt: Timestamp?
|
||||||
let endedAt: Timestamp?
|
let endedAt: Timestamp?
|
||||||
|
let lastActivity: String?
|
||||||
let progressSummary: String?
|
let progressSummary: String?
|
||||||
let terminalSummary: String?
|
let terminalSummary: String?
|
||||||
let error: String?
|
let error: String?
|
||||||
@@ -70,9 +71,9 @@ struct MobileBackgroundTask: Decodable, Identifiable, Equatable {
|
|||||||
|
|
||||||
var output: String? {
|
var output: String? {
|
||||||
let candidates = if self.status == "failed" || self.status == "timed_out" {
|
let candidates = if self.status == "failed" || self.status == "timed_out" {
|
||||||
[self.error, self.terminalSummary, self.progressSummary]
|
[self.error, self.terminalSummary, self.lastActivity, self.progressSummary]
|
||||||
} else {
|
} else {
|
||||||
[self.terminalSummary, self.error, self.progressSummary]
|
[self.terminalSummary, self.error, self.lastActivity, self.progressSummary]
|
||||||
}
|
}
|
||||||
return candidates.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty }.first
|
return candidates.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines).nilIfEmpty }.first
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ struct BackgroundTasksScreenTests {
|
|||||||
#expect(task.activityMilliseconds > 0)
|
#expect(task.activityMilliseconds > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func `running task prefers live activity over progress summary`() throws {
|
||||||
|
let data = Data(#"""
|
||||||
|
{
|
||||||
|
"id":"task-live",
|
||||||
|
"status":"running",
|
||||||
|
"runtime":"subagent",
|
||||||
|
"lastActivity":"Editing the shared transcript",
|
||||||
|
"progressSummary":"Earlier milestone"
|
||||||
|
}
|
||||||
|
"""#.utf8)
|
||||||
|
|
||||||
|
let task = try JSONDecoder().decode(MobileBackgroundTask.self, from: data)
|
||||||
|
|
||||||
|
#expect(task.output == "Editing the shared transcript")
|
||||||
|
}
|
||||||
|
|
||||||
@Test func `groups active work and deduplicates newest task snapshot`() throws {
|
@Test func `groups active work and deduplicates newest task snapshot`() throws {
|
||||||
let recent = try self.task(id: "finished", status: "completed", updatedAt: 4000)
|
let recent = try self.task(id: "finished", status: "completed", updatedAt: 4000)
|
||||||
let stale = try self.task(id: "running", status: "running", updatedAt: 2000)
|
let stale = try self.task(id: "running", status: "running", updatedAt: 2000)
|
||||||
|
|||||||
@@ -672,6 +672,13 @@ struct MacGatewayChatTransport: OpenClawChatTransport {
|
|||||||
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
return try JSONDecoder().decode(QuestionListResult.self, from: data).questions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listTasks(sessionKey: String, agentID: String?) async throws -> [TaskSummary] {
|
||||||
|
let data = try await connection.request(OpenClawChatGatewayRequests.tasksList(
|
||||||
|
sessionKey: sessionKey,
|
||||||
|
agentID: agentID))
|
||||||
|
return try JSONDecoder().decode(TasksListResult.self, from: data).tasks
|
||||||
|
}
|
||||||
|
|
||||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||||
let data = try await connection.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
let data = try await connection.request(OpenClawChatGatewayRequests.questionGet(id: id))
|
||||||
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
return try JSONDecoder().decode(QuestionGetResult.self, from: data).question
|
||||||
|
|||||||
@@ -162,18 +162,23 @@ public enum OpenClawChatGatewayPayloadCodec {
|
|||||||
as: OpenClawAgentEventPayload.self)
|
as: OpenClawAgentEventPayload.self)
|
||||||
else { return nil }
|
else { return nil }
|
||||||
return .agent(agent)
|
return .agent(agent)
|
||||||
|
default:
|
||||||
|
return self.secondaryEvent(from: frame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func secondaryEvent(from frame: EventFrame) -> OpenClawChatTransportEvent? {
|
||||||
|
guard let payload = frame.payload else { return nil }
|
||||||
|
switch frame.event {
|
||||||
|
case "task":
|
||||||
|
return (try? GatewayPayloadDecoding.decode(payload, as: OpenClawChatTaskEvent.self))
|
||||||
|
.map(OpenClawChatTransportEvent.task)
|
||||||
case "question.requested":
|
case "question.requested":
|
||||||
guard let payload = frame.payload,
|
return (try? GatewayPayloadDecoding.decode(payload, as: QuestionRecord.self))
|
||||||
let question = try? GatewayPayloadDecoding.decode(payload, as: QuestionRecord.self)
|
.map(OpenClawChatTransportEvent.questionRequested)
|
||||||
else { return nil }
|
|
||||||
return .questionRequested(question)
|
|
||||||
case "question.resolved":
|
case "question.resolved":
|
||||||
guard let payload = frame.payload,
|
return (try? GatewayPayloadDecoding.decode(payload, as: OpenClawQuestionResolvedEvent.self))
|
||||||
let resolved = try? GatewayPayloadDecoding.decode(
|
.map(OpenClawChatTransportEvent.questionResolved)
|
||||||
payload,
|
|
||||||
as: OpenClawQuestionResolvedEvent.self)
|
|
||||||
else { return nil }
|
|
||||||
return .questionResolved(resolved)
|
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,22 @@ public enum OpenClawChatGatewayRequests {
|
|||||||
OpenClawChatGatewayRequest(method: "question.list", timeoutMs: self.defaultTimeoutMs)
|
OpenClawChatGatewayRequest(method: "question.list", timeoutMs: self.defaultTimeoutMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static func tasksList(
|
||||||
|
sessionKey: String,
|
||||||
|
agentID: String?,
|
||||||
|
limit: Int = 200) -> OpenClawChatGatewayRequest
|
||||||
|
{
|
||||||
|
var params: [String: AnyCodable] = [
|
||||||
|
"sessionKey": AnyCodable(sessionKey),
|
||||||
|
"limit": AnyCodable(limit),
|
||||||
|
]
|
||||||
|
self.add(agentID, to: ¶ms, key: "agentId")
|
||||||
|
return OpenClawChatGatewayRequest(
|
||||||
|
method: "tasks.list",
|
||||||
|
params: params,
|
||||||
|
timeoutMs: self.defaultTimeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
public static func questionGet(id: String) -> OpenClawChatGatewayRequest {
|
public static func questionGet(id: String) -> OpenClawChatGatewayRequest {
|
||||||
OpenClawChatGatewayRequest(
|
OpenClawChatGatewayRequest(
|
||||||
method: "question.get",
|
method: "question.get",
|
||||||
|
|||||||
@@ -503,7 +503,8 @@ private struct ChatMessageBody: View {
|
|||||||
details: self.message.details,
|
details: self.message.details,
|
||||||
resultText: self.primaryText,
|
resultText: self.primaryText,
|
||||||
isError: self.message.isError ?? false,
|
isError: self.message.isError ?? false,
|
||||||
isPending: false)]
|
isPending: false,
|
||||||
|
liveDiffStat: nil)]
|
||||||
}
|
}
|
||||||
guard self.message.role.lowercased() == "assistant" else { return [] }
|
guard self.message.role.lowercased() == "assistant" else { return [] }
|
||||||
return ChatToolActivity.items(calls: self.toolCalls, results: self.inlineToolResults)
|
return ChatToolActivity.items(calls: self.toolCalls, results: self.inlineToolResults)
|
||||||
@@ -1012,7 +1013,8 @@ struct ChatPendingToolsBubble: View {
|
|||||||
details: nil,
|
details: nil,
|
||||||
resultText: nil,
|
resultText: nil,
|
||||||
isError: false,
|
isError: false,
|
||||||
isPending: true)
|
isPending: true,
|
||||||
|
liveDiffStat: call.diffStat)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -877,6 +877,7 @@ public struct OpenClawChatPendingToolCall: Identifiable, Hashable, Sendable {
|
|||||||
public let args: AnyCodable?
|
public let args: AnyCodable?
|
||||||
public let startedAt: Double?
|
public let startedAt: Double?
|
||||||
public let isError: Bool?
|
public let isError: Bool?
|
||||||
|
let diffStat: ChatToolDiffStat?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct OpenClawGatewayHealthOK: Codable, Sendable {
|
public struct OpenClawGatewayHealthOK: Codable, Sendable {
|
||||||
|
|||||||
@@ -0,0 +1,256 @@
|
|||||||
|
import Foundation
|
||||||
|
import OpenClawKit
|
||||||
|
import OpenClawProtocol
|
||||||
|
|
||||||
|
enum ChatSubagentActivityStatus: String, Sendable {
|
||||||
|
case queued
|
||||||
|
case running
|
||||||
|
case completed
|
||||||
|
case failed
|
||||||
|
case cancelled
|
||||||
|
case timedOut = "timed_out"
|
||||||
|
|
||||||
|
var isWorking: Bool {
|
||||||
|
self == .queued || self == .running
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ChatSubagentActivitySource: Sendable {
|
||||||
|
case event
|
||||||
|
case snapshot
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatSubagentActivity: Identifiable, Equatable, Sendable {
|
||||||
|
let id: String
|
||||||
|
let status: ChatSubagentActivityStatus
|
||||||
|
let snippet: String?
|
||||||
|
let diffStat: ChatToolDiffStat?
|
||||||
|
let startedAt: Double
|
||||||
|
let updatedAt: Double
|
||||||
|
let endedAt: Double?
|
||||||
|
let terminalObservedAt: Double?
|
||||||
|
let childSessionKey: String?
|
||||||
|
let terminalSummary: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatSubagentActivityPresentation: Equatable, Sendable {
|
||||||
|
let rows: [ChatSubagentActivity]
|
||||||
|
let hiddenWorkingCount: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ChatSubagentActivityState: Equatable, Sendable {
|
||||||
|
private(set) var activitiesByID: [String: ChatSubagentActivity] = [:]
|
||||||
|
|
||||||
|
mutating func upsert(
|
||||||
|
_ task: TaskSummary,
|
||||||
|
nowMilliseconds: Double,
|
||||||
|
source: ChatSubagentActivitySource = .event)
|
||||||
|
{
|
||||||
|
guard let status = task.status.stringValue.flatMap(ChatSubagentActivityStatus.init(rawValue:))
|
||||||
|
else { return }
|
||||||
|
let previous = self.activitiesByID[task.id]
|
||||||
|
let fallbackSnippet = Self.firstNonBlank(task.lastactivity, task.progresssummary, task.lasttoolname)
|
||||||
|
let snippet = if !status.isWorking, previous != nil, Self.nonBlank(task.lastactivity) == nil {
|
||||||
|
previous?.snippet
|
||||||
|
} else {
|
||||||
|
fallbackSnippet ?? previous?.snippet
|
||||||
|
}
|
||||||
|
let endedAt = Self.timestampMilliseconds(task.endedat) ?? previous?.endedAt
|
||||||
|
let updatedAt = Self.timestampMilliseconds(task.updatedat)
|
||||||
|
?? previous?.updatedAt
|
||||||
|
?? endedAt
|
||||||
|
?? nowMilliseconds
|
||||||
|
let terminalObservedAt: Double? = if status.isWorking {
|
||||||
|
nil
|
||||||
|
} else if let previous, !previous.status.isWorking {
|
||||||
|
previous.terminalObservedAt
|
||||||
|
} else {
|
||||||
|
source == .event ? nowMilliseconds : endedAt ?? updatedAt
|
||||||
|
}
|
||||||
|
self.activitiesByID[task.id] = ChatSubagentActivity(
|
||||||
|
id: task.id,
|
||||||
|
status: status,
|
||||||
|
snippet: snippet,
|
||||||
|
diffStat: Self.diffStat(task.diffstat) ?? previous?.diffStat,
|
||||||
|
startedAt: Self.timestampMilliseconds(task.startedat)
|
||||||
|
?? previous?.startedAt
|
||||||
|
?? Self.timestampMilliseconds(task.createdat)
|
||||||
|
?? nowMilliseconds,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
endedAt: endedAt,
|
||||||
|
terminalObservedAt: terminalObservedAt,
|
||||||
|
childSessionKey: Self.nonBlank(task.childsessionkey) ?? previous?.childSessionKey,
|
||||||
|
terminalSummary: Self.nonBlank(task.terminalsummary) ?? previous?.terminalSummary)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func remove(taskID: String) {
|
||||||
|
self.activitiesByID[taskID] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func removeAll() {
|
||||||
|
self.activitiesByID.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func removeExpired(
|
||||||
|
nowMilliseconds: Double,
|
||||||
|
retentionMilliseconds: Double = 60000)
|
||||||
|
{
|
||||||
|
self.activitiesByID = self.activitiesByID.filter { _, activity in
|
||||||
|
activity.status.isWorking ||
|
||||||
|
(activity.terminalObservedAt.map { nowMilliseconds - $0 < retentionMilliseconds } ?? false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentation(limit: Int = 5) -> ChatSubagentActivityPresentation {
|
||||||
|
let sorted = self.activitiesByID.values.sorted { lhs, rhs in
|
||||||
|
if lhs.updatedAt != rhs.updatedAt {
|
||||||
|
return lhs.updatedAt > rhs.updatedAt
|
||||||
|
}
|
||||||
|
return lhs.id < rhs.id
|
||||||
|
}
|
||||||
|
let ordered = sorted.filter(\.status.isWorking) + sorted.filter { !$0.status.isWorking }
|
||||||
|
let rows = Array(ordered.prefix(limit))
|
||||||
|
let visibleIDs = Set(rows.map(\.id))
|
||||||
|
let hiddenWorkingCount = ordered.count { activity in
|
||||||
|
activity.status == .running && !visibleIDs.contains(activity.id)
|
||||||
|
}
|
||||||
|
return ChatSubagentActivityPresentation(
|
||||||
|
rows: rows,
|
||||||
|
hiddenWorkingCount: hiddenWorkingCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextExpiryMilliseconds(retentionMilliseconds: Double = 60000) -> Double? {
|
||||||
|
self.activitiesByID.values
|
||||||
|
.filter { !$0.status.isWorking }
|
||||||
|
.compactMap(\.terminalObservedAt)
|
||||||
|
.map { $0 + retentionMilliseconds }
|
||||||
|
.min()
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func diffStat(_ value: [String: AnyCodable]?) -> ChatToolDiffStat? {
|
||||||
|
guard let added = value?["added"]?.intValue,
|
||||||
|
let removed = value?["removed"]?.intValue,
|
||||||
|
added >= 0,
|
||||||
|
removed >= 0
|
||||||
|
else { return nil }
|
||||||
|
let files = value?["files"]?.intValue
|
||||||
|
return ChatToolDiffStat(
|
||||||
|
files: files.map { max(0, $0) },
|
||||||
|
added: added,
|
||||||
|
removed: removed)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func timestampMilliseconds(_ value: AnyCodable?) -> Double? {
|
||||||
|
if let number = value?.doubleValue, number >= 0 { return number }
|
||||||
|
guard let raw = value?.stringValue?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
!raw.isEmpty
|
||||||
|
else { return nil }
|
||||||
|
if let number = Double(raw), number >= 0 { return number }
|
||||||
|
let fractional = ISO8601DateFormatter()
|
||||||
|
fractional.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||||
|
let date = fractional.date(from: raw) ?? ISO8601DateFormatter().date(from: raw)
|
||||||
|
return date.map { $0.timeIntervalSince1970 * 1000 }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func firstNonBlank(_ values: String?...) -> String? {
|
||||||
|
values.lazy.compactMap(self.nonBlank).first
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func nonBlank(_ value: String?) -> String? {
|
||||||
|
let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return trimmed?.isEmpty == false ? trimmed : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OpenClawChatViewModel {
|
||||||
|
func handleTaskEvent(_ event: OpenClawChatTaskEvent) {
|
||||||
|
switch event {
|
||||||
|
case let .upserted(task):
|
||||||
|
self.foldSubagentTask(task)
|
||||||
|
case let .deleted(taskID):
|
||||||
|
self.updateSubagentActivityState { $0.remove(taskID: taskID) }
|
||||||
|
case .restored:
|
||||||
|
let session = self.currentSessionSnapshot()
|
||||||
|
Task { await self.refreshSubagentActivities(sessionSnapshot: session) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshSubagentActivities(sessionSnapshot: SessionSnapshot) async {
|
||||||
|
let baseline = self.subagentActivityState.activitiesByID
|
||||||
|
let tasks: [TaskSummary]
|
||||||
|
do {
|
||||||
|
tasks = try await self.transport.listTasks(
|
||||||
|
sessionKey: sessionSnapshot.key,
|
||||||
|
agentID: sessionSnapshot.deliveryAgentID)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard self.isCurrentSession(sessionSnapshot) else { return }
|
||||||
|
self.updateSubagentActivityState { state in
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
for task in tasks where self.isCurrentSubagentTask(task) {
|
||||||
|
// A task event received during this request is newer than its list snapshot.
|
||||||
|
guard state.activitiesByID[task.id] == baseline[task.id] else { continue }
|
||||||
|
state.upsert(task, nowMilliseconds: now, source: .snapshot)
|
||||||
|
}
|
||||||
|
state.removeExpired(nowMilliseconds: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearSubagentActivities() {
|
||||||
|
self.subagentActivityCleanupTask?.cancel()
|
||||||
|
self.subagentActivityCleanupTask = nil
|
||||||
|
self.subagentActivityState.removeAll()
|
||||||
|
self.subagentActivities = []
|
||||||
|
self.hiddenWorkingSubagentCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
private func foldSubagentTask(_ task: TaskSummary) {
|
||||||
|
guard self.isCurrentSubagentTask(task) else { return }
|
||||||
|
self.updateSubagentActivityState { state in
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
state.upsert(task, nowMilliseconds: now)
|
||||||
|
state.removeExpired(nowMilliseconds: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isCurrentSubagentTask(_ task: TaskSummary) -> Bool {
|
||||||
|
guard task.runtime == "subagent",
|
||||||
|
let requesterSessionKey = task.sessionkey
|
||||||
|
else { return false }
|
||||||
|
return self.matchesCurrentSessionKey(
|
||||||
|
incoming: requesterSessionKey,
|
||||||
|
agentId: task.agentid,
|
||||||
|
current: self.sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateSubagentActivityState(
|
||||||
|
_ update: (inout ChatSubagentActivityState) -> Void)
|
||||||
|
{
|
||||||
|
let previous = self.subagentActivityState
|
||||||
|
update(&self.subagentActivityState)
|
||||||
|
guard self.subagentActivityState != previous else { return }
|
||||||
|
let presentation = self.subagentActivityState.presentation()
|
||||||
|
self.subagentActivities = presentation.rows
|
||||||
|
self.hiddenWorkingSubagentCount = presentation.hiddenWorkingCount
|
||||||
|
self.scheduleSubagentActivityCleanup()
|
||||||
|
self.markTimelineChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleSubagentActivityCleanup() {
|
||||||
|
self.subagentActivityCleanupTask?.cancel()
|
||||||
|
guard let expiry = self.subagentActivityState.nextExpiryMilliseconds() else {
|
||||||
|
self.subagentActivityCleanupTask = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let now = Date().timeIntervalSince1970 * 1000
|
||||||
|
let delay = max(0, Int64((expiry - now).rounded(.up)))
|
||||||
|
self.subagentActivityCleanupTask = Task { [weak self] in
|
||||||
|
try? await Task.sleep(for: .milliseconds(delay))
|
||||||
|
guard !Task.isCancelled, let self else { return }
|
||||||
|
self.updateSubagentActivityState { state in
|
||||||
|
state.removeExpired(nowMilliseconds: Date().timeIntervalSince1970 * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ChatSubagentActivityList: View {
|
||||||
|
let activities: [ChatSubagentActivity]
|
||||||
|
let hiddenWorkingCount: Int
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
ForEach(self.activities) { activity in
|
||||||
|
ChatSubagentActivityRow(activity: activity)
|
||||||
|
}
|
||||||
|
if self.hiddenWorkingCount > 0 {
|
||||||
|
Text(verbatim: String(
|
||||||
|
format: String(localized: "+%1$lld more working"),
|
||||||
|
Int64(self.hiddenWorkingCount)))
|
||||||
|
.font(OpenClawChatTypography.caption2)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.padding(.leading, 35)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(4)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ChatSubagentActivityRow: View {
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
|
||||||
|
let activity: ChatSubagentActivity
|
||||||
|
|
||||||
|
private var detail: String? {
|
||||||
|
if self.activity.status.isWorking {
|
||||||
|
return self.activity.snippet
|
||||||
|
}
|
||||||
|
return self.activity.terminalSummary ?? self.activity.snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
private var title: LocalizedStringResource {
|
||||||
|
switch self.activity.status {
|
||||||
|
case .queued, .running:
|
||||||
|
"Subagent working"
|
||||||
|
case .completed:
|
||||||
|
"Subagent finished"
|
||||||
|
case .failed, .timedOut:
|
||||||
|
"Subagent failed"
|
||||||
|
case .cancelled:
|
||||||
|
"Subagent cancelled"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var titleColor: Color {
|
||||||
|
switch self.activity.status {
|
||||||
|
case .failed, .timedOut:
|
||||||
|
OpenClawChatTheme.danger
|
||||||
|
case .queued, .running, .completed, .cancelled:
|
||||||
|
OpenClawChatTheme.assistantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 7) {
|
||||||
|
if self.activity.status.isWorking {
|
||||||
|
ChatWorkingClawView(seed: self.activity.id)
|
||||||
|
} else {
|
||||||
|
Image(systemName: self.activity.status == .completed ? "checkmark" : "xmark")
|
||||||
|
.font(.system(size: 11, weight: .bold))
|
||||||
|
.foregroundStyle(
|
||||||
|
self.activity.status == .completed
|
||||||
|
? OpenClawChatTheme.success
|
||||||
|
: OpenClawChatTheme.danger)
|
||||||
|
.frame(width: 28, height: 24)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(self.title)
|
||||||
|
.font(OpenClawChatTypography.footnoteSemiBold)
|
||||||
|
.foregroundStyle(self.titleColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
|
||||||
|
if let detail = self.detail {
|
||||||
|
Text(verbatim: detail)
|
||||||
|
.font(OpenClawChatTypography.mono(size: 12, relativeTo: .footnote))
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.contentTransition(.opacity)
|
||||||
|
.animation(
|
||||||
|
self.reduceMotion ? nil : .easeOut(duration: 0.16),
|
||||||
|
value: detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let stat = self.activity.diffStat {
|
||||||
|
ChatDiffStatChips(stat: stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 3)
|
||||||
|
.accessibilityElement(children: .combine)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ struct ChatToolActivityItem: Identifiable, Equatable {
|
|||||||
let resultText: String?
|
let resultText: String?
|
||||||
let isError: Bool
|
let isError: Bool
|
||||||
let isPending: Bool
|
let isPending: Bool
|
||||||
|
let liveDiffStat: ChatToolDiffStat?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChatToolActivity {
|
enum ChatToolActivity {
|
||||||
@@ -34,7 +35,8 @@ enum ChatToolActivity {
|
|||||||
details: result?.details,
|
details: result?.details,
|
||||||
resultText: result?.text,
|
resultText: result?.text,
|
||||||
isError: result?.isError ?? false,
|
isError: result?.isError ?? false,
|
||||||
isPending: false)
|
isPending: false,
|
||||||
|
liveDiffStat: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(contentsOf: remainingResults.map { index, result in
|
items.append(contentsOf: remainingResults.map { index, result in
|
||||||
@@ -45,7 +47,8 @@ enum ChatToolActivity {
|
|||||||
details: result.details,
|
details: result.details,
|
||||||
resultText: result.text,
|
resultText: result.text,
|
||||||
isError: result.isError ?? false,
|
isError: result.isError ?? false,
|
||||||
isPending: false)
|
isPending: false,
|
||||||
|
liveDiffStat: nil)
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
@@ -106,6 +109,10 @@ struct ChatToolActivityRow: View {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var displayedDiffStat: ChatToolDiffStat? {
|
||||||
|
self.item.isPending ? self.item.liveDiffStat ?? self.resolvedDiff?.stat : self.resolvedDiff?.stat
|
||||||
|
}
|
||||||
|
|
||||||
init(item: ChatToolActivityItem) {
|
init(item: ChatToolActivityItem) {
|
||||||
self.item = item
|
self.item = item
|
||||||
self.resolvedDiff = ChatToolDiff.resolveDiff(
|
self.resolvedDiff = ChatToolDiff.resolveDiff(
|
||||||
@@ -222,15 +229,8 @@ struct ChatToolActivityRow: View {
|
|||||||
.truncationMode(.tail)
|
.truncationMode(.tail)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let stat = self.resolvedDiff?.stat {
|
if let stat = self.displayedDiffStat {
|
||||||
Text(verbatim: "+\(stat.added)")
|
ChatDiffStatChips(stat: stat)
|
||||||
.font(OpenClawChatTypography.mono(size: 12, relativeTo: .footnote))
|
|
||||||
.foregroundStyle(OpenClawChatTheme.success.opacity(0.9))
|
|
||||||
.lineLimit(1)
|
|
||||||
Text(verbatim: "−\(stat.removed)")
|
|
||||||
.font(OpenClawChatTypography.mono(size: 12, relativeTo: .footnote))
|
|
||||||
.foregroundStyle(OpenClawChatTheme.danger.opacity(0.9))
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(minLength: 0)
|
Spacer(minLength: 0)
|
||||||
@@ -398,6 +398,21 @@ struct ChatToolActivityRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ChatDiffStatChips: View {
|
||||||
|
let stat: ChatToolDiffStat
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
Text(verbatim: "+\(self.stat.added)")
|
||||||
|
.foregroundStyle(OpenClawChatTheme.success.opacity(0.9))
|
||||||
|
Text(verbatim: "−\(self.stat.removed)")
|
||||||
|
.foregroundStyle(OpenClawChatTheme.danger.opacity(0.9))
|
||||||
|
}
|
||||||
|
.font(OpenClawChatTypography.mono(size: 12, relativeTo: .footnote))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ChatToolActivityList: View {
|
struct ChatToolActivityList: View {
|
||||||
let items: [ChatToolActivityItem]
|
let items: [ChatToolActivityItem]
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,16 @@ struct ChatToolDiffLine: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChatToolDiffStat: Equatable, Sendable {
|
struct ChatToolDiffStat: Equatable, Hashable, Sendable {
|
||||||
|
let files: Int?
|
||||||
let added: Int
|
let added: Int
|
||||||
let removed: Int
|
let removed: Int
|
||||||
|
|
||||||
|
init(files: Int? = nil, added: Int, removed: Int) {
|
||||||
|
self.files = files
|
||||||
|
self.added = added
|
||||||
|
self.removed = removed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChatToolDiff {
|
enum ChatToolDiff {
|
||||||
|
|||||||
@@ -9,12 +9,44 @@ public enum OpenClawChatTransportEvent: Sendable {
|
|||||||
case chat(OpenClawChatEventPayload)
|
case chat(OpenClawChatEventPayload)
|
||||||
case sessionMessage(OpenClawSessionMessageEventPayload)
|
case sessionMessage(OpenClawSessionMessageEventPayload)
|
||||||
case agent(OpenClawAgentEventPayload)
|
case agent(OpenClawAgentEventPayload)
|
||||||
|
case task(OpenClawChatTaskEvent)
|
||||||
case questionRequested(QuestionRecord)
|
case questionRequested(QuestionRecord)
|
||||||
case questionResolved(OpenClawQuestionResolvedEvent)
|
case questionResolved(OpenClawQuestionResolvedEvent)
|
||||||
case routeChanged
|
case routeChanged
|
||||||
case seqGap
|
case seqGap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum OpenClawChatTaskEvent: Sendable, Decodable {
|
||||||
|
case upserted(TaskSummary)
|
||||||
|
case deleted(taskID: String)
|
||||||
|
case restored
|
||||||
|
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let action = try container.decode(Action.self, forKey: .action)
|
||||||
|
switch action {
|
||||||
|
case .upserted:
|
||||||
|
self = try .upserted(container.decode(TaskSummary.self, forKey: .task))
|
||||||
|
case .deleted:
|
||||||
|
self = try .deleted(taskID: container.decode(String.self, forKey: .taskID))
|
||||||
|
case .restored:
|
||||||
|
self = .restored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Action: String, Decodable {
|
||||||
|
case upserted
|
||||||
|
case deleted
|
||||||
|
case restored
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case action
|
||||||
|
case task
|
||||||
|
case taskID = "taskId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct OpenClawQuestionResolvedEvent: Codable, Sendable {
|
public struct OpenClawQuestionResolvedEvent: Codable, Sendable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let status: QuestionStatus
|
public let status: QuestionStatus
|
||||||
@@ -727,6 +759,7 @@ public protocol OpenClawChatTransport: Sendable {
|
|||||||
|
|
||||||
func requestHealth(timeoutMs: Int) async throws -> Bool
|
func requestHealth(timeoutMs: Int) async throws -> Bool
|
||||||
func listQuestions() async throws -> [QuestionRecord]
|
func listQuestions() async throws -> [QuestionRecord]
|
||||||
|
func listTasks(sessionKey: String, agentID: String?) async throws -> [TaskSummary]
|
||||||
func getQuestion(id: String) async throws -> QuestionRecord
|
func getQuestion(id: String) async throws -> QuestionRecord
|
||||||
func resolveQuestion(id: String, answers: [String: [String]]) async throws
|
func resolveQuestion(id: String, answers: [String: [String]]) async throws
|
||||||
func cancelQuestion(id: String) async throws
|
func cancelQuestion(id: String) async throws
|
||||||
@@ -772,6 +805,10 @@ extension OpenClawChatTransport {
|
|||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func listTasks(sessionKey _: String, agentID _: String?) async throws -> [TaskSummary] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
public func getQuestion(id _: String) async throws -> QuestionRecord {
|
public func getQuestion(id _: String) async throws -> QuestionRecord {
|
||||||
throw NSError(
|
throw NSError(
|
||||||
domain: "OpenClawChatTransport",
|
domain: "OpenClawChatTransport",
|
||||||
|
|||||||
@@ -500,6 +500,13 @@ public struct OpenClawChatView: View {
|
|||||||
.equatable()
|
.equatable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.displayOptions.contains(.toolActivity), !self.viewModel.subagentActivities.isEmpty {
|
||||||
|
ChatSubagentActivityList(
|
||||||
|
activities: self.viewModel.subagentActivities,
|
||||||
|
hiddenWorkingCount: self.viewModel.hiddenWorkingSubagentCount)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
if self.displayOptions.contains(.toolActivity), !self.viewModel.pendingToolCalls.isEmpty {
|
if self.displayOptions.contains(.toolActivity), !self.viewModel.pendingToolCalls.isEmpty {
|
||||||
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
|
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
|
||||||
.equatable()
|
.equatable()
|
||||||
@@ -819,6 +826,7 @@ public struct OpenClawChatView: View {
|
|||||||
|
|
||||||
private var hasVisibleTransientContent: Bool {
|
private var hasVisibleTransientContent: Bool {
|
||||||
self.viewModel.hasBlockingRunActivity ||
|
self.viewModel.hasBlockingRunActivity ||
|
||||||
|
(self.displayOptions.contains(.toolActivity) && !self.viewModel.subagentActivities.isEmpty) ||
|
||||||
(self.displayOptions.contains(.toolActivity) && !self.viewModel.pendingToolCalls.isEmpty) ||
|
(self.displayOptions.contains(.toolActivity) && !self.viewModel.pendingToolCalls.isEmpty) ||
|
||||||
self.hasVisibleStreamingAssistantText ||
|
self.hasVisibleStreamingAssistantText ||
|
||||||
!self.viewModel.visibleQuestionCards.isEmpty
|
!self.viewModel.visibleQuestionCards.isEmpty
|
||||||
@@ -875,6 +883,7 @@ public struct OpenClawChatView: View {
|
|||||||
self.viewModel.messages.isEmpty &&
|
self.viewModel.messages.isEmpty &&
|
||||||
!self.hasVisibleStreamingAssistantText &&
|
!self.hasVisibleStreamingAssistantText &&
|
||||||
!self.viewModel.hasBlockingRunActivity &&
|
!self.viewModel.hasBlockingRunActivity &&
|
||||||
|
self.viewModel.subagentActivities.isEmpty &&
|
||||||
self.viewModel.pendingToolCalls.isEmpty
|
self.viewModel.pendingToolCalls.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -927,6 +936,7 @@ public struct OpenClawChatView: View {
|
|||||||
guard self.hasPerformedInitialScroll else { return }
|
guard self.hasPerformedInitialScroll else { return }
|
||||||
if self.viewModel.messages.isEmpty,
|
if self.viewModel.messages.isEmpty,
|
||||||
!self.viewModel.hasBlockingRunActivity,
|
!self.viewModel.hasBlockingRunActivity,
|
||||||
|
self.viewModel.subagentActivities.isEmpty,
|
||||||
self.viewModel.pendingToolCalls.isEmpty,
|
self.viewModel.pendingToolCalls.isEmpty,
|
||||||
self.viewModel.streamingAssistantText == nil
|
self.viewModel.streamingAssistantText == nil
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,6 +3,35 @@ import Foundation
|
|||||||
extension OpenClawChatViewModel {
|
extension OpenClawChatViewModel {
|
||||||
public static let verboseLevelOptions = ["off", "on", "full"]
|
public static let verboseLevelOptions = ["off", "on", "full"]
|
||||||
|
|
||||||
|
public var modelPickerSections: ChatModelPickerSections {
|
||||||
|
let defaultProvider = ChatModelPickerStore.resolvedDefaultProvider(
|
||||||
|
provider: self.sessionDefaults?.modelProvider,
|
||||||
|
model: self.sessionDefaults?.model)
|
||||||
|
return ChatModelPickerStore.sections(
|
||||||
|
choices: self.modelChoices,
|
||||||
|
favorites: self.modelPickerFavorites,
|
||||||
|
recents: self.modelPickerRecents,
|
||||||
|
defaultProvider: defaultProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isDefaultModel(_ model: OpenClawChatModelChoice) -> Bool {
|
||||||
|
ChatModelPickerStore.isDefaultModel(
|
||||||
|
model,
|
||||||
|
defaultProvider: self.sessionDefaults?.modelProvider,
|
||||||
|
defaultModel: self.sessionDefaults?.model)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isSelectedModelPinned: Bool {
|
||||||
|
self.modelSelectionID != Self.defaultModelSelectionID &&
|
||||||
|
self.modelPickerFavorites.contains(self.modelSelectionID)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func toggleSelectedModelPinned() {
|
||||||
|
guard self.modelSelectionID != Self.defaultModelSelectionID else { return }
|
||||||
|
self.modelPickerStore.toggleFavorite(self.modelSelectionID)
|
||||||
|
self.modelPickerFavorites = self.modelPickerStore.favorites
|
||||||
|
}
|
||||||
|
|
||||||
public var thinkingSelectionID: String {
|
public var thinkingSelectionID: String {
|
||||||
self.thinkingOverrideIsInherited ? Self.inheritedThinkingSelectionID : self.thinkingLevel
|
self.thinkingOverrideIsInherited ? Self.inheritedThinkingSelectionID : self.thinkingLevel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ extension OpenClawChatViewModel {
|
|||||||
self.handleSessionMessageEvent(message)
|
self.handleSessionMessageEvent(message)
|
||||||
case let .agent(agent):
|
case let .agent(agent):
|
||||||
self.handleAgentEvent(agent)
|
self.handleAgentEvent(agent)
|
||||||
|
case let .task(task):
|
||||||
|
self.handleTaskEvent(task)
|
||||||
case let .questionRequested(question):
|
case let .questionRequested(question):
|
||||||
self.upsertQuestion(question)
|
self.upsertQuestion(question)
|
||||||
self.reconcileQuestionsAfterEvent()
|
self.reconcileQuestionsAfterEvent()
|
||||||
@@ -56,6 +58,8 @@ extension OpenClawChatViewModel {
|
|||||||
self.swarmEnabled = false
|
self.swarmEnabled = false
|
||||||
self.resetSwarmProgress()
|
self.resetSwarmProgress()
|
||||||
Task { [weak self] in await self?.refreshSwarmCapability() }
|
Task { [weak self] in await self?.refreshSwarmCapability() }
|
||||||
|
let session = self.currentSessionSnapshot()
|
||||||
|
Task { [weak self] in await self?.refreshSubagentActivities(sessionSnapshot: session) }
|
||||||
case .seqGap:
|
case .seqGap:
|
||||||
self.errorText = nil
|
self.errorText = nil
|
||||||
self.swarmEnabled = false
|
self.swarmEnabled = false
|
||||||
@@ -72,6 +76,7 @@ extension OpenClawChatViewModel {
|
|||||||
// Question refresh is best-effort and must not delay transcript
|
// Question refresh is best-effort and must not delay transcript
|
||||||
// recovery behind a slow gateway round trip.
|
// recovery behind a slow gateway round trip.
|
||||||
Task { await self.refreshQuestions() }
|
Task { await self.refreshQuestions() }
|
||||||
|
Task { await self.refreshSubagentActivities(sessionSnapshot: context.session) }
|
||||||
Task {
|
Task {
|
||||||
await self.refreshHistoryAfterRun(historyRequest: context)
|
await self.refreshHistoryAfterRun(historyRequest: context)
|
||||||
await self.pollHealthIfNeeded(force: true, sessionSnapshot: context.session)
|
await self.pollHealthIfNeeded(force: true, sessionSnapshot: context.session)
|
||||||
@@ -649,7 +654,23 @@ extension OpenClawChatViewModel {
|
|||||||
name: name,
|
name: name,
|
||||||
args: args,
|
args: args,
|
||||||
startedAt: evt.ts.map(Double.init) ?? Date().timeIntervalSince1970 * 1000,
|
startedAt: evt.ts.map(Double.init) ?? Date().timeIntervalSince1970 * 1000,
|
||||||
isError: nil)
|
isError: nil,
|
||||||
|
diffStat: nil)
|
||||||
|
} else if phase == "input_delta",
|
||||||
|
let pending = self.pendingToolCallsById[toolCallId],
|
||||||
|
let diff = evt.data["diff"]?.dictionaryValue,
|
||||||
|
let added = diff["added"]?.intValue,
|
||||||
|
let removed = diff["removed"]?.intValue,
|
||||||
|
added >= 0,
|
||||||
|
removed >= 0
|
||||||
|
{
|
||||||
|
self.pendingToolCallsById[toolCallId] = OpenClawChatPendingToolCall(
|
||||||
|
toolCallId: pending.toolCallId,
|
||||||
|
name: pending.name,
|
||||||
|
args: pending.args,
|
||||||
|
startedAt: pending.startedAt,
|
||||||
|
isError: pending.isError,
|
||||||
|
diffStat: ChatToolDiffStat(added: added, removed: removed))
|
||||||
} else if phase == "result" {
|
} else if phase == "result" {
|
||||||
self.pendingToolCallsById[toolCallId] = nil
|
self.pendingToolCallsById[toolCallId] = nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ public final class OpenClawChatViewModel {
|
|||||||
var prefersExplicitVerboseLevel: Bool
|
var prefersExplicitVerboseLevel: Bool
|
||||||
public private(set) var modelSelectionID: String = "__default__"
|
public private(set) var modelSelectionID: String = "__default__"
|
||||||
public private(set) var modelChoices: [OpenClawChatModelChoice] = []
|
public private(set) var modelChoices: [OpenClawChatModelChoice] = []
|
||||||
private var modelPickerFavorites: [String]
|
var modelPickerFavorites: [String]
|
||||||
private var modelPickerRecents: [String]
|
var modelPickerRecents: [String]
|
||||||
/// Setters are module-internal for the sending extension's command catalog.
|
/// Setters are module-internal for the sending extension's command catalog.
|
||||||
public internal(set) var slashCommands: [OpenClawChatCommandChoice] = []
|
public internal(set) var slashCommands: [OpenClawChatCommandChoice] = []
|
||||||
public internal(set) var isLoadingSlashCommands = false
|
public internal(set) var isLoadingSlashCommands = false
|
||||||
@@ -111,6 +111,8 @@ public final class OpenClawChatViewModel {
|
|||||||
public private(set) var streamingAssistantText: String?
|
public private(set) var streamingAssistantText: String?
|
||||||
|
|
||||||
public private(set) var pendingToolCalls: [OpenClawChatPendingToolCall] = []
|
public private(set) var pendingToolCalls: [OpenClawChatPendingToolCall] = []
|
||||||
|
var subagentActivities: [ChatSubagentActivity] = []
|
||||||
|
var hiddenWorkingSubagentCount = 0
|
||||||
public internal(set) var planSteps: [OpenClawChatPlanStep] = []
|
public internal(set) var planSteps: [OpenClawChatPlanStep] = []
|
||||||
public internal(set) var planExplanation: String?
|
public internal(set) var planExplanation: String?
|
||||||
var planRunId: String?
|
var planRunId: String?
|
||||||
@@ -153,7 +155,7 @@ public final class OpenClawChatViewModel {
|
|||||||
let transcriptCache: (any OpenClawChatTranscriptCache)?
|
let transcriptCache: (any OpenClawChatTranscriptCache)?
|
||||||
let outbox: (any OpenClawChatCommandOutbox)?
|
let outbox: (any OpenClawChatCommandOutbox)?
|
||||||
@ObservationIgnored
|
@ObservationIgnored
|
||||||
private let modelPickerStore: ChatModelPickerStore
|
let modelPickerStore: ChatModelPickerStore
|
||||||
/// Per-message outbox display state; rows without an entry are normal
|
/// Per-message outbox display state; rows without an entry are normal
|
||||||
/// transcript rows. Observable so bubbles update when flush progresses.
|
/// transcript rows. Observable so bubbles update when flush progresses.
|
||||||
public internal(set) var outboxStatesByMessageID: [UUID: OpenClawChatOutboxMessageState] = [:]
|
public internal(set) var outboxStatesByMessageID: [UUID: OpenClawChatOutboxMessageState] = [:]
|
||||||
@@ -444,6 +446,11 @@ public final class OpenClawChatViewModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObservationIgnored
|
||||||
|
var subagentActivityState = ChatSubagentActivityState()
|
||||||
|
@ObservationIgnored
|
||||||
|
var subagentActivityCleanupTask: Task<Void, Never>?
|
||||||
|
|
||||||
var lastHealthPollAt: Date?
|
var lastHealthPollAt: Date?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@@ -544,6 +551,7 @@ public final class OpenClawChatViewModel {
|
|||||||
}
|
}
|
||||||
self.outboxChangesTask?.cancel()
|
self.outboxChangesTask?.cancel()
|
||||||
self.activeSessionRunIndicatorTimeoutTask?.cancel()
|
self.activeSessionRunIndicatorTimeoutTask?.cancel()
|
||||||
|
self.subagentActivityCleanupTask?.cancel()
|
||||||
self.questionRefreshRetryTask?.cancel()
|
self.questionRefreshRetryTask?.cancel()
|
||||||
for (_, task) in self.questionExpiryTasks {
|
for (_, task) in self.questionExpiryTasks {
|
||||||
task.cancel()
|
task.cancel()
|
||||||
@@ -561,35 +569,6 @@ public final class OpenClawChatViewModel {
|
|||||||
startBootstrap()
|
startBootstrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
public var modelPickerSections: ChatModelPickerSections {
|
|
||||||
let defaultProvider = ChatModelPickerStore.resolvedDefaultProvider(
|
|
||||||
provider: self.sessionDefaults?.modelProvider,
|
|
||||||
model: self.sessionDefaults?.model)
|
|
||||||
return ChatModelPickerStore.sections(
|
|
||||||
choices: self.modelChoices,
|
|
||||||
favorites: self.modelPickerFavorites,
|
|
||||||
recents: self.modelPickerRecents,
|
|
||||||
defaultProvider: defaultProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func isDefaultModel(_ model: OpenClawChatModelChoice) -> Bool {
|
|
||||||
ChatModelPickerStore.isDefaultModel(
|
|
||||||
model,
|
|
||||||
defaultProvider: self.sessionDefaults?.modelProvider,
|
|
||||||
defaultModel: self.sessionDefaults?.model)
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isSelectedModelPinned: Bool {
|
|
||||||
self.modelSelectionID != Self.defaultModelSelectionID &&
|
|
||||||
self.modelPickerFavorites.contains(self.modelSelectionID)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func toggleSelectedModelPinned() {
|
|
||||||
guard self.modelSelectionID != Self.defaultModelSelectionID else { return }
|
|
||||||
self.modelPickerStore.toggleFavorite(self.modelSelectionID)
|
|
||||||
self.modelPickerFavorites = self.modelPickerStore.favorites
|
|
||||||
}
|
|
||||||
|
|
||||||
public func resumeFromForeground() {
|
public func resumeFromForeground() {
|
||||||
Task { await self.refreshRunStateAfterForeground() }
|
Task { await self.refreshRunStateAfterForeground() }
|
||||||
}
|
}
|
||||||
@@ -957,6 +936,7 @@ extension OpenClawChatViewModel {
|
|||||||
|
|
||||||
Task { [weak self] in await self?.refreshQuestions() }
|
Task { [weak self] in await self?.refreshQuestions() }
|
||||||
Task { [weak self] in await self?.refreshSwarmCapability(sessionSnapshot: context.session) }
|
Task { [weak self] in await self?.refreshSwarmCapability(sessionSnapshot: context.session) }
|
||||||
|
Task { [weak self] in await self?.refreshSubagentActivities(sessionSnapshot: context.session) }
|
||||||
|
|
||||||
let payload = try await transport.requestHistory(sessionKey: context.session.key)
|
let payload = try await transport.requestHistory(sessionKey: context.session.key)
|
||||||
guard self.isCurrentBootstrap(context) else { return }
|
guard self.isCurrentBootstrap(context) else { return }
|
||||||
@@ -1277,6 +1257,7 @@ extension OpenClawChatViewModel {
|
|||||||
resetOutboxPresentationForSessionSwitch()
|
resetOutboxPresentationForSessionSwitch()
|
||||||
self.sessionId = nil
|
self.sessionId = nil
|
||||||
self.pendingToolCallsById = [:]
|
self.pendingToolCallsById = [:]
|
||||||
|
self.clearSubagentActivities()
|
||||||
self.updateStreamingAssistantText(nil)
|
self.updateStreamingAssistantText(nil)
|
||||||
clearPlan()
|
clearPlan()
|
||||||
self.updateActiveSessionRunWithoutChatSnapshot(false)
|
self.updateActiveSessionRunWithoutChatSnapshot(false)
|
||||||
|
|||||||
@@ -523,6 +523,34 @@ struct ChatGatewayPayloadCodecTests {
|
|||||||
#expect(digest.runid == "run-1")
|
#expect(digest.runid == "run-1")
|
||||||
#expect(digest.revision == 2)
|
#expect(digest.revision == 2)
|
||||||
|
|
||||||
|
let task = EventFrame(
|
||||||
|
type: "event",
|
||||||
|
event: "task",
|
||||||
|
payload: AnyCodable([
|
||||||
|
"action": AnyCodable("upserted"),
|
||||||
|
"task": AnyCodable([
|
||||||
|
"id": AnyCodable("task-1"),
|
||||||
|
"runtime": AnyCodable("subagent"),
|
||||||
|
"status": AnyCodable("running"),
|
||||||
|
"sessionKey": AnyCodable("agent:main:main"),
|
||||||
|
"lastActivity": AnyCodable("Editing ChatView.swift"),
|
||||||
|
"diffStat": AnyCodable([
|
||||||
|
"files": AnyCodable(1),
|
||||||
|
"added": AnyCodable(8),
|
||||||
|
"removed": AnyCodable(2),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
]))
|
||||||
|
guard case let .task(.upserted(summary)) = OpenClawChatGatewayPayloadCodec.event(from: task)
|
||||||
|
else {
|
||||||
|
Issue.record("expected task upsert")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#expect(summary.id == "task-1")
|
||||||
|
#expect(summary.sessionkey == "agent:main:main")
|
||||||
|
#expect(summary.lastactivity == "Editing ChatView.swift")
|
||||||
|
#expect(summary.diffstat?["added"]?.intValue == 8)
|
||||||
|
|
||||||
#expect(OpenClawChatGatewayPayloadCodec.event(from: EventFrame(
|
#expect(OpenClawChatGatewayPayloadCodec.event(from: EventFrame(
|
||||||
type: "event",
|
type: "event",
|
||||||
event: "unknown")) == nil)
|
event: "unknown")) == nil)
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import OpenClawKit
|
||||||
|
import OpenClawProtocol
|
||||||
|
import Testing
|
||||||
|
@testable import OpenClawChatUI
|
||||||
|
|
||||||
|
@Suite("Chat subagent activity")
|
||||||
|
struct ChatSubagentActivityTests {
|
||||||
|
@Test func `terminal snapshot retains live fields and expires after sixty seconds`() throws {
|
||||||
|
var state = ChatSubagentActivityState()
|
||||||
|
state.upsert(
|
||||||
|
self.task(
|
||||||
|
id: "task-1",
|
||||||
|
status: "running",
|
||||||
|
lastActivity: "Applying patch",
|
||||||
|
diffStat: ["files": 1, "added": 7, "removed": 2]),
|
||||||
|
nowMilliseconds: 1000)
|
||||||
|
state.upsert(
|
||||||
|
self.task(
|
||||||
|
id: "task-1",
|
||||||
|
status: "completed",
|
||||||
|
progressSummary: "Earlier milestone",
|
||||||
|
terminalSummary: "Done",
|
||||||
|
endedAt: 2000),
|
||||||
|
nowMilliseconds: 2000)
|
||||||
|
|
||||||
|
let retained = try #require(state.presentation().rows.first)
|
||||||
|
#expect(retained.status == .completed)
|
||||||
|
#expect(retained.snippet == "Applying patch")
|
||||||
|
#expect(retained.diffStat == ChatToolDiffStat(files: 1, added: 7, removed: 2))
|
||||||
|
|
||||||
|
state.removeExpired(nowMilliseconds: 61999)
|
||||||
|
#expect(state.presentation().rows.count == 1)
|
||||||
|
state.removeExpired(nowMilliseconds: 62000)
|
||||||
|
#expect(state.presentation().rows.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func `caps rows at five and counts only hidden working tasks`() {
|
||||||
|
var state = ChatSubagentActivityState()
|
||||||
|
for index in 0..<7 {
|
||||||
|
state.upsert(
|
||||||
|
self.task(id: "working-\(index)", status: "running", startedAt: Double(index)),
|
||||||
|
nowMilliseconds: Double(index))
|
||||||
|
}
|
||||||
|
state.upsert(
|
||||||
|
self.task(id: "finished", status: "completed", endedAt: 10),
|
||||||
|
nowMilliseconds: 10)
|
||||||
|
|
||||||
|
let presentation = state.presentation()
|
||||||
|
#expect(presentation.rows.map(\.id) == (2..<7).reversed().map { "working-\($0)" })
|
||||||
|
#expect(presentation.hiddenWorkingCount == 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func task(
|
||||||
|
id: String,
|
||||||
|
status: String,
|
||||||
|
lastActivity: String? = nil,
|
||||||
|
progressSummary: String? = nil,
|
||||||
|
terminalSummary: String? = nil,
|
||||||
|
diffStat: [String: Int]? = nil,
|
||||||
|
startedAt: Double = 0,
|
||||||
|
updatedAt: Double? = nil,
|
||||||
|
endedAt: Double? = nil) -> TaskSummary
|
||||||
|
{
|
||||||
|
TaskSummary(
|
||||||
|
id: id,
|
||||||
|
runtime: "subagent",
|
||||||
|
status: AnyCodable(status),
|
||||||
|
agentid: "main",
|
||||||
|
sessionkey: "agent:main:main",
|
||||||
|
updatedat: AnyCodable(updatedAt ?? startedAt),
|
||||||
|
startedat: AnyCodable(startedAt),
|
||||||
|
endedat: endedAt.map(AnyCodable.init),
|
||||||
|
lastactivity: lastActivity,
|
||||||
|
diffstat: diffStat?.mapValues(AnyCodable.init),
|
||||||
|
progresssummary: progressSummary,
|
||||||
|
terminalsummary: terminalSummary)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,8 @@ struct ChatToolActivityTests {
|
|||||||
details: nil,
|
details: nil,
|
||||||
resultText: "done",
|
resultText: "done",
|
||||||
isError: false,
|
isError: false,
|
||||||
isPending: false)])
|
isPending: false,
|
||||||
|
liveDiffStat: nil)])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `appends orphan result`() {
|
@Test func `appends orphan result`() {
|
||||||
@@ -31,7 +32,8 @@ struct ChatToolActivityTests {
|
|||||||
details: nil,
|
details: nil,
|
||||||
resultText: "orphaned",
|
resultText: "orphaned",
|
||||||
isError: false,
|
isError: false,
|
||||||
isPending: false)])
|
isPending: false,
|
||||||
|
liveDiffStat: nil)])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `preserves call order`() {
|
@Test func `preserves call order`() {
|
||||||
@@ -61,7 +63,8 @@ struct ChatToolActivityTests {
|
|||||||
details: nil,
|
details: nil,
|
||||||
resultText: nil,
|
resultText: nil,
|
||||||
isError: false,
|
isError: false,
|
||||||
isPending: false)])
|
isPending: false,
|
||||||
|
liveDiffStat: nil)])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func `threads paired result details`() {
|
@Test func `threads paired result details`() {
|
||||||
|
|||||||
@@ -96,6 +96,33 @@ private func usageEvent(runId: String, outputTokens: Int, seq: Int) -> OpenClawA
|
|||||||
data: ["outputTokens": AnyCodable(outputTokens)])
|
data: ["outputTokens": AnyCodable(outputTokens)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func subagentTaskSummary(
|
||||||
|
id: String,
|
||||||
|
status: String,
|
||||||
|
sessionKey: String = "agent:main:main",
|
||||||
|
lastActivity: String? = nil,
|
||||||
|
progressSummary: String? = nil,
|
||||||
|
terminalSummary: String? = nil,
|
||||||
|
diffStat: [String: AnyCodable]? = nil,
|
||||||
|
startedAt: Double = 1000,
|
||||||
|
endedAt: Double? = nil) -> TaskSummary
|
||||||
|
{
|
||||||
|
TaskSummary(
|
||||||
|
id: id,
|
||||||
|
runtime: "subagent",
|
||||||
|
status: AnyCodable(status),
|
||||||
|
agentid: "main",
|
||||||
|
sessionkey: sessionKey,
|
||||||
|
childsessionkey: "agent:main:subagent:\(id)",
|
||||||
|
updatedat: AnyCodable(endedAt ?? startedAt),
|
||||||
|
startedat: AnyCodable(startedAt),
|
||||||
|
endedat: endedAt.map(AnyCodable.init),
|
||||||
|
lastactivity: lastActivity,
|
||||||
|
diffstat: diffStat,
|
||||||
|
progresssummary: progressSummary,
|
||||||
|
terminalsummary: terminalSummary)
|
||||||
|
}
|
||||||
|
|
||||||
private func lifecycleSessionEntry(
|
private func lifecycleSessionEntry(
|
||||||
key: String,
|
key: String,
|
||||||
updatedAt: Double,
|
updatedAt: Double,
|
||||||
@@ -702,6 +729,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
private let swarmEnabledHook: (@Sendable (String) async throws -> Bool)?
|
private let swarmEnabledHook: (@Sendable (String) async throws -> Bool)?
|
||||||
private let listChildSessionsHook: (@Sendable (String) async throws -> [OpenClawChatSessionEntry])?
|
private let listChildSessionsHook: (@Sendable (String) async throws -> [OpenClawChatSessionEntry])?
|
||||||
private let listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])?
|
private let listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])?
|
||||||
|
private let listTasksHook: (@Sendable (String, String?) async throws -> [TaskSummary])?
|
||||||
private let getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)?
|
private let getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)?
|
||||||
private let cancelQuestionHook: (@Sendable (String) async throws -> Void)?
|
private let cancelQuestionHook: (@Sendable (String) async throws -> Void)?
|
||||||
private let healthResponses: [Bool]
|
private let healthResponses: [Bool]
|
||||||
@@ -738,6 +766,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
swarmEnabledHook: (@Sendable (String) async throws -> Bool)? = nil,
|
swarmEnabledHook: (@Sendable (String) async throws -> Bool)? = nil,
|
||||||
listChildSessionsHook: (@Sendable (String) async throws -> [OpenClawChatSessionEntry])? = nil,
|
listChildSessionsHook: (@Sendable (String) async throws -> [OpenClawChatSessionEntry])? = nil,
|
||||||
listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])? = nil,
|
listQuestionsHook: (@Sendable () async throws -> [QuestionRecord])? = nil,
|
||||||
|
listTasksHook: (@Sendable (String, String?) async throws -> [TaskSummary])? = nil,
|
||||||
getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)? = nil,
|
getQuestionHook: (@Sendable (String) async throws -> QuestionRecord)? = nil,
|
||||||
cancelQuestionHook: (@Sendable (String) async throws -> Void)? = nil,
|
cancelQuestionHook: (@Sendable (String) async throws -> Void)? = nil,
|
||||||
healthResponses: [Bool] = [true])
|
healthResponses: [Bool] = [true])
|
||||||
@@ -768,6 +797,7 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
self.swarmEnabledHook = swarmEnabledHook
|
self.swarmEnabledHook = swarmEnabledHook
|
||||||
self.listChildSessionsHook = listChildSessionsHook
|
self.listChildSessionsHook = listChildSessionsHook
|
||||||
self.listQuestionsHook = listQuestionsHook
|
self.listQuestionsHook = listQuestionsHook
|
||||||
|
self.listTasksHook = listTasksHook
|
||||||
self.getQuestionHook = getQuestionHook
|
self.getQuestionHook = getQuestionHook
|
||||||
self.cancelQuestionHook = cancelQuestionHook
|
self.cancelQuestionHook = cancelQuestionHook
|
||||||
self.healthResponses = healthResponses
|
self.healthResponses = healthResponses
|
||||||
@@ -1069,6 +1099,10 @@ private final class TestChatTransport: @unchecked Sendable, OpenClawChatTranspor
|
|||||||
try await self.listQuestionsHook?() ?? []
|
try await self.listQuestionsHook?() ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listTasks(sessionKey: String, agentID: String?) async throws -> [TaskSummary] {
|
||||||
|
try await self.listTasksHook?(sessionKey, agentID) ?? []
|
||||||
|
}
|
||||||
|
|
||||||
func getQuestion(id: String) async throws -> QuestionRecord {
|
func getQuestion(id: String) async throws -> QuestionRecord {
|
||||||
guard let getQuestionHook else {
|
guard let getQuestionHook else {
|
||||||
throw NSError(
|
throw NSError(
|
||||||
@@ -1425,6 +1459,110 @@ private actor SwarmCapabilityScript {
|
|||||||
|
|
||||||
@Suite(.serialized)
|
@Suite(.serialized)
|
||||||
struct ChatViewModelTests {
|
struct ChatViewModelTests {
|
||||||
|
@Test func `bootstrap fills subagent activity from the current session task list`() async throws {
|
||||||
|
let transport = TestChatTransport(
|
||||||
|
historyResponses: [historyPayload()],
|
||||||
|
listTasksHook: { sessionKey, agentID in
|
||||||
|
guard sessionKey == "main", agentID == "main" else { return [] }
|
||||||
|
return [subagentTaskSummary(
|
||||||
|
id: "listed",
|
||||||
|
status: "running",
|
||||||
|
progressSummary: "Restored from task list")]
|
||||||
|
})
|
||||||
|
let viewModel = await MainActor.run {
|
||||||
|
OpenClawChatViewModel(
|
||||||
|
sessionKey: "main",
|
||||||
|
transport: transport,
|
||||||
|
activeAgentId: "main")
|
||||||
|
}
|
||||||
|
|
||||||
|
await MainActor.run { viewModel.load() }
|
||||||
|
try await waitUntil("listed subagent activity") {
|
||||||
|
await MainActor.run { viewModel.subagentActivities.map(\.id) == ["listed"] }
|
||||||
|
}
|
||||||
|
|
||||||
|
#expect(await MainActor.run { viewModel.subagentActivities.first?.snippet } ==
|
||||||
|
"Restored from task list")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func `subagent task events filter by session and retain terminal activity`() {
|
||||||
|
let viewModel = OpenClawChatViewModel(
|
||||||
|
sessionKey: "main",
|
||||||
|
transport: TestChatTransport(historyResponses: []),
|
||||||
|
activeAgentId: "main")
|
||||||
|
let liveDiff = [
|
||||||
|
"files": AnyCodable(2),
|
||||||
|
"added": AnyCodable(9),
|
||||||
|
"removed": AnyCodable(3),
|
||||||
|
]
|
||||||
|
|
||||||
|
viewModel.handleTransportEvent(.task(.upserted(subagentTaskSummary(
|
||||||
|
id: "foreign",
|
||||||
|
status: "running",
|
||||||
|
sessionKey: "agent:main:other",
|
||||||
|
lastActivity: "Must stay hidden"))))
|
||||||
|
viewModel.handleTransportEvent(.task(.upserted(subagentTaskSummary(
|
||||||
|
id: "owned",
|
||||||
|
status: "running",
|
||||||
|
lastActivity: "Editing shared chat",
|
||||||
|
diffStat: liveDiff))))
|
||||||
|
|
||||||
|
#expect(viewModel.subagentActivities.map(\.id) == ["owned"])
|
||||||
|
#expect(viewModel.subagentActivities[0].snippet == "Editing shared chat")
|
||||||
|
#expect(viewModel.subagentActivities[0].diffStat == ChatToolDiffStat(
|
||||||
|
files: 2,
|
||||||
|
added: 9,
|
||||||
|
removed: 3))
|
||||||
|
|
||||||
|
viewModel.handleTransportEvent(.task(.upserted(subagentTaskSummary(
|
||||||
|
id: "owned",
|
||||||
|
status: "completed",
|
||||||
|
progressSummary: "Older milestone",
|
||||||
|
terminalSummary: "Finished cleanly",
|
||||||
|
endedAt: Date().timeIntervalSince1970 * 1000))))
|
||||||
|
|
||||||
|
#expect(viewModel.subagentActivities[0].status == .completed)
|
||||||
|
#expect(viewModel.subagentActivities[0].snippet == "Editing shared chat")
|
||||||
|
#expect(viewModel.subagentActivities[0].terminalSummary == "Finished cleanly")
|
||||||
|
#expect(viewModel.subagentActivities[0].diffStat?.added == 9)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func `tool input delta updates the matching pending edit diff`() {
|
||||||
|
let viewModel = OpenClawChatViewModel(
|
||||||
|
sessionKey: "main",
|
||||||
|
transport: TestChatTransport(historyResponses: []))
|
||||||
|
viewModel.sessionId = "run-1"
|
||||||
|
viewModel.handleTransportEvent(.agent(OpenClawAgentEventPayload(
|
||||||
|
runId: "run-1",
|
||||||
|
seq: 1,
|
||||||
|
stream: "tool",
|
||||||
|
ts: 1000,
|
||||||
|
data: [
|
||||||
|
"phase": AnyCodable("start"),
|
||||||
|
"name": AnyCodable("apply_patch"),
|
||||||
|
"toolCallId": AnyCodable("tool-1"),
|
||||||
|
"args": AnyCodable(["patch": "*** Begin Patch"]),
|
||||||
|
])))
|
||||||
|
viewModel.handleTransportEvent(.agent(OpenClawAgentEventPayload(
|
||||||
|
runId: "run-1",
|
||||||
|
seq: 2,
|
||||||
|
stream: "tool",
|
||||||
|
ts: 1001,
|
||||||
|
data: [
|
||||||
|
"phase": AnyCodable("input_delta"),
|
||||||
|
"name": AnyCodable("apply_patch"),
|
||||||
|
"toolCallId": AnyCodable("tool-1"),
|
||||||
|
"diff": AnyCodable([
|
||||||
|
"added": AnyCodable(12),
|
||||||
|
"removed": AnyCodable(4),
|
||||||
|
]),
|
||||||
|
])))
|
||||||
|
|
||||||
|
#expect(viewModel.pendingToolCalls.first?.diffStat == ChatToolDiffStat(
|
||||||
|
added: 12,
|
||||||
|
removed: 4))
|
||||||
|
}
|
||||||
|
|
||||||
@Test @MainActor func `transient Swarm capability failure preserves state and retries until explicit false`() async throws {
|
@Test @MainActor func `transient Swarm capability failure preserves state and retries until explicit false`() async throws {
|
||||||
let script = SwarmCapabilityScript([.value(true), .failure, .value(false)])
|
let script = SwarmCapabilityScript([.value(true), .failure, .value(false)])
|
||||||
var child = sessionEntry(key: "agent:main:child", updatedAt: 1)
|
var child = sessionEntry(key: "agent:main:child", updatedAt: 1)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
"session.typing": "Collaborative typing state is a Control UI-only ephemeral indicator; iOS does not render it.",
|
"session.typing": "Collaborative typing state is a Control UI-only ephemeral indicator; iOS does not render it.",
|
||||||
"shutdown": "iOS relies on socket close plus reconnect/backoff instead of the shutdown notice.",
|
"shutdown": "iOS relies on socket close plus reconnect/backoff instead of the shutdown notice.",
|
||||||
"skills.changed": "Skills settings is a macOS operator surface; iOS does not expose skill management yet.",
|
"skills.changed": "Skills settings is a macOS operator surface; iOS does not expose skill management yet.",
|
||||||
"task": "Background task activity is not surfaced in the iOS app.",
|
|
||||||
"task.suggestion": "Task suggestion cards are a Control UI-only surface; iOS does not render them.",
|
"task.suggestion": "Task suggestion cards are a Control UI-only surface; iOS does not render them.",
|
||||||
"terminal.data": "Embedded terminal is a web/desktop surface; iOS has no terminal client.",
|
"terminal.data": "Embedded terminal is a web/desktop surface; iOS has no terminal client.",
|
||||||
"terminal.exit": "Embedded terminal is a web/desktop surface; iOS has no terminal client.",
|
"terminal.exit": "Embedded terminal is a web/desktop surface; iOS has no terminal client.",
|
||||||
|
|||||||
Reference in New Issue
Block a user