@swarmclawai/swarmclaw 1.2.8 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +39 -6
  2. package/package.json +2 -2
  3. package/src/app/agents/[id]/page.tsx +1 -18
  4. package/src/app/api/activity/route.ts +9 -23
  5. package/src/app/api/agents/route.ts +17 -1
  6. package/src/app/api/agents/thread-route.test.ts +0 -1
  7. package/src/app/api/approvals/route.test.ts +6 -22
  8. package/src/app/api/approvals/route.ts +13 -5
  9. package/src/app/api/connectors/route.ts +2 -2
  10. package/src/app/api/credentials/[id]/route.ts +2 -0
  11. package/src/app/api/credentials/route.ts +4 -1
  12. package/src/app/api/goals/[id]/route.ts +28 -0
  13. package/src/app/api/goals/route.ts +33 -0
  14. package/src/app/api/portability/export/route.ts +8 -0
  15. package/src/app/api/portability/import/route.test.ts +80 -0
  16. package/src/app/api/portability/import/route.ts +28 -0
  17. package/src/app/api/protocols/templates/[id]/route.ts +2 -1
  18. package/src/app/api/protocols/templates/route.ts +2 -1
  19. package/src/app/api/settings/route.ts +13 -2
  20. package/src/app/api/wallets/[id]/route.ts +15 -157
  21. package/src/app/api/wallets/generate/route.ts +22 -0
  22. package/src/app/api/wallets/route.test.ts +147 -0
  23. package/src/app/api/wallets/route.ts +13 -95
  24. package/src/app/autonomy/page.tsx +2 -57
  25. package/src/app/home/page.tsx +3 -0
  26. package/src/app/protocols/page.tsx +2 -21
  27. package/src/app/settings/page.tsx +0 -9
  28. package/src/app/wallets/page.tsx +105 -5
  29. package/src/cli/index.js +32 -33
  30. package/src/cli/spec.js +26 -27
  31. package/src/components/agents/agent-sheet.tsx +2 -40
  32. package/src/components/agents/inspector-panel.tsx +0 -83
  33. package/src/components/chat/chat-card.tsx +0 -31
  34. package/src/components/chat/message-bubble.tsx +1 -108
  35. package/src/components/connectors/connector-sheet.tsx +25 -1
  36. package/src/components/layout/sidebar-rail.tsx +6 -10
  37. package/src/components/projects/project-detail.tsx +3 -35
  38. package/src/components/projects/tabs/overview-tab.tsx +3 -59
  39. package/src/components/projects/tabs/work-tab.tsx +7 -77
  40. package/src/components/protocols/structured-session-launcher.tsx +1 -22
  41. package/src/components/shared/connector-platform-icon.tsx +1 -0
  42. package/src/components/tasks/task-card.tsx +4 -34
  43. package/src/components/tasks/task-sheet.tsx +6 -36
  44. package/src/components/wallets/wallet-list.tsx +150 -0
  45. package/src/lib/app/navigation.test.ts +0 -13
  46. package/src/lib/app/navigation.ts +2 -7
  47. package/src/lib/app/view-constants.ts +14 -19
  48. package/src/lib/server/activity/activity-log.ts +16 -1
  49. package/src/lib/server/agents/agent-service.ts +24 -11
  50. package/src/lib/server/agents/agent-thread-session.ts +0 -1
  51. package/src/lib/server/agents/delegation-advisory.test.ts +0 -1
  52. package/src/lib/server/agents/delegation-jobs.test.ts +0 -69
  53. package/src/lib/server/agents/delegation-jobs.ts +0 -25
  54. package/src/lib/server/agents/main-agent-loop.ts +1 -49
  55. package/src/lib/server/agents/subagent-runtime.ts +0 -1
  56. package/src/lib/server/approval-match.ts +14 -85
  57. package/src/lib/server/approvals/approval-hooks.ts +81 -0
  58. package/src/lib/server/approvals.test.ts +6 -6
  59. package/src/lib/server/approvals.ts +11 -6
  60. package/src/lib/server/autonomy/supervisor-reflection.test.ts +0 -1
  61. package/src/lib/server/builtin-extensions.ts +0 -2
  62. package/src/lib/server/capability-router.test.ts +0 -2
  63. package/src/lib/server/chat-execution/chat-execution-tool-events.test.ts +14 -14
  64. package/src/lib/server/chat-execution/chat-execution-types.ts +0 -2
  65. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -2
  66. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -30
  67. package/src/lib/server/chat-execution/chat-turn-finalization.ts +1 -36
  68. package/src/lib/server/chat-execution/chat-turn-preparation.ts +2 -22
  69. package/src/lib/server/chat-execution/iteration-event-handler.ts +0 -24
  70. package/src/lib/server/chat-execution/message-classifier.test.ts +0 -45
  71. package/src/lib/server/chat-execution/message-classifier.ts +1 -16
  72. package/src/lib/server/chat-execution/prompt-builder.test.ts +0 -1
  73. package/src/lib/server/chat-execution/prompt-builder.ts +0 -30
  74. package/src/lib/server/chat-execution/prompt-sections.ts +0 -1
  75. package/src/lib/server/chat-execution/situational-awareness.test.ts +2 -73
  76. package/src/lib/server/chat-execution/situational-awareness.ts +4 -38
  77. package/src/lib/server/chat-execution/stream-agent-chat.test.ts +8 -123
  78. package/src/lib/server/chat-execution/stream-agent-chat.ts +1 -5
  79. package/src/lib/server/chat-execution/stream-continuation.test.ts +4 -52
  80. package/src/lib/server/chat-execution/stream-continuation.ts +6 -48
  81. package/src/lib/server/chatrooms/session-mailbox.ts +0 -10
  82. package/src/lib/server/chats/chat-session-service.ts +3 -5
  83. package/src/lib/server/connectors/connector-inbound.ts +0 -1
  84. package/src/lib/server/connectors/connector-lifecycle.ts +19 -3
  85. package/src/lib/server/connectors/connector-service.ts +39 -9
  86. package/src/lib/server/connectors/swarmdock-bidding.ts +74 -0
  87. package/src/lib/server/connectors/swarmdock-payloads.test.ts +85 -0
  88. package/src/lib/server/connectors/swarmdock-secret.test.ts +128 -0
  89. package/src/lib/server/connectors/swarmdock-secret.ts +152 -0
  90. package/src/lib/server/connectors/swarmdock-tasks.ts +127 -0
  91. package/src/lib/server/connectors/swarmdock.ts +285 -0
  92. package/src/lib/server/execution-brief.test.ts +2 -25
  93. package/src/lib/server/execution-brief.ts +30 -35
  94. package/src/lib/server/execution-engine/task-attempt.ts +0 -1
  95. package/src/lib/server/goals/goal-repository.ts +19 -0
  96. package/src/lib/server/goals/goal-service.ts +143 -0
  97. package/src/lib/server/persistence/storage-context.ts +0 -5
  98. package/src/lib/server/portability/export.ts +109 -0
  99. package/src/lib/server/portability/import.ts +159 -0
  100. package/src/lib/server/protocols/protocol-normalization.ts +0 -4
  101. package/src/lib/server/protocols/protocol-queries.ts +0 -6
  102. package/src/lib/server/protocols/protocol-run-lifecycle.ts +4 -32
  103. package/src/lib/server/protocols/protocol-service.ts +0 -1
  104. package/src/lib/server/protocols/protocol-step-helpers.ts +0 -4
  105. package/src/lib/server/protocols/protocol-step-processors.ts +0 -6
  106. package/src/lib/server/protocols/protocol-swarm.ts +0 -2
  107. package/src/lib/server/protocols/protocol-types.ts +0 -2
  108. package/src/lib/server/provider-health.ts +0 -9
  109. package/src/lib/server/runtime/daemon-state/core.ts +0 -9
  110. package/src/lib/server/runtime/daemon-state.test.ts +0 -35
  111. package/src/lib/server/runtime/heartbeat-service.ts +3 -23
  112. package/src/lib/server/runtime/queue/core.ts +11 -33
  113. package/src/lib/server/runtime/runtime-storage-write-paths.test.ts +6 -6
  114. package/src/lib/server/runtime/scheduler.ts +0 -13
  115. package/src/lib/server/runtime/session-run-manager/drain.ts +0 -24
  116. package/src/lib/server/runtime/session-run-manager/enqueue.ts +0 -1
  117. package/src/lib/server/runtime/session-run-manager/queries.ts +0 -1
  118. package/src/lib/server/runtime/session-run-manager/recovery.ts +0 -1
  119. package/src/lib/server/runtime/session-run-manager.test.ts +0 -28
  120. package/src/lib/server/session-tools/crud.ts +0 -14
  121. package/src/lib/server/session-tools/delegate.ts +0 -4
  122. package/src/lib/server/session-tools/index.ts +0 -4
  123. package/src/lib/server/session-tools/team-context.ts +0 -3
  124. package/src/lib/server/storage-normalization.ts +13 -0
  125. package/src/lib/server/storage.ts +75 -45
  126. package/src/lib/server/tasks/task-checkout.ts +59 -0
  127. package/src/lib/server/tasks/task-lifecycle.ts +2 -0
  128. package/src/lib/server/tasks/task-route-service.ts +4 -26
  129. package/src/lib/server/tasks/task-service.ts +0 -7
  130. package/src/lib/server/tool-aliases.ts +0 -1
  131. package/src/lib/server/tool-capability-policy-advanced.test.ts +4 -4
  132. package/src/lib/server/tool-capability-policy.ts +0 -2
  133. package/src/lib/server/tool-planning.ts +0 -12
  134. package/src/lib/server/universal-tool-access.ts +0 -1
  135. package/src/lib/server/usage/cost-rollup.ts +124 -0
  136. package/src/lib/server/usage/usage-repository.ts +6 -0
  137. package/src/lib/server/wallets/wallet-crypto.ts +33 -0
  138. package/src/lib/server/wallets/wallet-repository.ts +24 -0
  139. package/src/lib/server/wallets/wallet-service.ts +119 -0
  140. package/src/lib/server/working-state/extraction.ts +8 -42
  141. package/src/lib/server/working-state/normalization.ts +10 -103
  142. package/src/lib/server/working-state/service.ts +12 -21
  143. package/src/lib/strip-internal-metadata.test.ts +1 -1
  144. package/src/lib/strip-internal-metadata.ts +1 -1
  145. package/src/lib/tool-definitions.ts +0 -1
  146. package/src/lib/validation/schemas.ts +36 -32
  147. package/src/lib/validation/server-schemas.ts +35 -0
  148. package/src/stores/slices/data-slice.ts +5 -1
  149. package/src/stores/slices/ui-slice.ts +0 -4
  150. package/src/types/agent.ts +10 -84
  151. package/src/types/app-settings.ts +6 -2
  152. package/src/types/approval.ts +3 -2
  153. package/src/types/connector.ts +1 -0
  154. package/src/types/goal.ts +30 -0
  155. package/src/types/index.ts +2 -1
  156. package/src/types/message.ts +0 -1
  157. package/src/types/misc.ts +2 -4
  158. package/src/types/protocol.ts +0 -2
  159. package/src/types/run.ts +0 -3
  160. package/src/types/session.ts +1 -51
  161. package/src/types/swarmdock.ts +29 -0
  162. package/src/types/task.ts +9 -3
  163. package/src/types/working-state.ts +2 -9
  164. package/src/views/settings/section-runtime-loop.tsx +0 -14
  165. package/src/app/api/canvas/[sessionId]/route.ts +0 -35
  166. package/src/app/api/missions/[id]/actions/route.ts +0 -31
  167. package/src/app/api/missions/[id]/events/route.ts +0 -14
  168. package/src/app/api/missions/[id]/route.ts +0 -10
  169. package/src/app/api/missions/route.test.ts +0 -244
  170. package/src/app/api/missions/route.ts +0 -57
  171. package/src/app/api/wallets/[id]/approve/route.ts +0 -79
  172. package/src/app/api/wallets/[id]/balance-history/route.ts +0 -18
  173. package/src/app/api/wallets/[id]/send/route.ts +0 -113
  174. package/src/app/api/wallets/[id]/transactions/route.ts +0 -18
  175. package/src/app/missions/[id]/page.tsx +0 -3
  176. package/src/app/missions/page.tsx +0 -685
  177. package/src/components/canvas/canvas-panel.tsx +0 -267
  178. package/src/components/wallets/wallet-approval-dialog.tsx +0 -107
  179. package/src/components/wallets/wallet-panel.tsx +0 -1010
  180. package/src/components/wallets/wallet-section.tsx +0 -260
  181. package/src/features/missions/queries.ts +0 -23
  182. package/src/lib/canvas-content.test.ts +0 -360
  183. package/src/lib/canvas-content.ts +0 -198
  184. package/src/lib/server/canvas-content.test.ts +0 -32
  185. package/src/lib/server/canvas-content.ts +0 -6
  186. package/src/lib/server/ethereum.ts +0 -591
  187. package/src/lib/server/evm-swap.ts +0 -476
  188. package/src/lib/server/missions/mission-intent.test.ts +0 -63
  189. package/src/lib/server/missions/mission-intent.ts +0 -569
  190. package/src/lib/server/missions/mission-repository.ts +0 -74
  191. package/src/lib/server/missions/mission-service/actions.ts +0 -6
  192. package/src/lib/server/missions/mission-service/bindings.ts +0 -9
  193. package/src/lib/server/missions/mission-service/context.ts +0 -4
  194. package/src/lib/server/missions/mission-service/core.ts +0 -2271
  195. package/src/lib/server/missions/mission-service/queries.ts +0 -12
  196. package/src/lib/server/missions/mission-service/recovery.ts +0 -5
  197. package/src/lib/server/missions/mission-service/ticks.ts +0 -9
  198. package/src/lib/server/missions/mission-service.test.ts +0 -888
  199. package/src/lib/server/missions/mission-service.ts +0 -6
  200. package/src/lib/server/session-tools/canvas.ts +0 -105
  201. package/src/lib/server/session-tools/wallet-tool.test.ts +0 -150
  202. package/src/lib/server/session-tools/wallet.ts +0 -1287
  203. package/src/lib/server/solana.ts +0 -327
  204. package/src/lib/server/wallet/wallet-execution.test.ts +0 -198
  205. package/src/lib/server/wallet/wallet-portfolio.test.ts +0 -98
  206. package/src/lib/server/wallet/wallet-portfolio.ts +0 -772
  207. package/src/lib/server/wallet/wallet-service.test.ts +0 -81
  208. package/src/lib/server/wallet/wallet-service.ts +0 -225
  209. package/src/lib/wallet/wallet-transactions.test.ts +0 -75
  210. package/src/lib/wallet/wallet-transactions.ts +0 -43
  211. package/src/lib/wallet/wallet.test.ts +0 -333
  212. package/src/lib/wallet/wallet.ts +0 -183
  213. package/src/types/mission.ts +0 -185
  214. package/src/views/settings/section-wallets.tsx +0 -35
@@ -1,685 +0,0 @@
1
- 'use client'
2
-
3
- import { useCallback, useEffect, useMemo, useState } from 'react'
4
- import { usePathname, useRouter, useSearchParams } from 'next/navigation'
5
- import { api } from '@/lib/app/api-client'
6
- import { useWs } from '@/hooks/use-ws'
7
- import { useAppStore } from '@/stores/use-app-store'
8
- import { FilterPill } from '@/components/ui/filter-pill'
9
- import { PageLoader } from '@/components/ui/page-loader'
10
- import { StatCard } from '@/components/ui/stat-card'
11
- import { StructuredSessionLauncher } from '@/components/protocols/structured-session-launcher'
12
- import { timeAgo } from '@/lib/time-format'
13
- import { getMissionPath } from '@/lib/app/navigation'
14
- import type {
15
- ApprovalRequest,
16
- BoardTask,
17
- Mission,
18
- MissionEvent,
19
- MissionPhase,
20
- MissionStatus,
21
- MissionSummary,
22
- SessionQueuedTurn,
23
- SessionRunRecord,
24
- } from '@/types'
25
-
26
- type MissionDetailResponse = {
27
- mission: Mission
28
- summary: MissionSummary
29
- parent: MissionSummary | null
30
- children: MissionSummary[]
31
- linkedTasks: BoardTask[]
32
- recentRuns: SessionRunRecord[]
33
- queuedTurns: SessionQueuedTurn[]
34
- approvals: ApprovalRequest[]
35
- events: MissionEvent[]
36
- }
37
-
38
- type MissionStatusFilter = 'all' | MissionStatus
39
- type MissionWaitKind = NonNullable<Mission['waitState']>['kind']
40
-
41
- function missionStatusTone(status: MissionStatus): string {
42
- if (status === 'completed') return 'text-emerald-300 bg-emerald-500/12 border-emerald-500/18'
43
- if (status === 'waiting') return 'text-amber-300 bg-amber-500/12 border-amber-500/18'
44
- if (status === 'failed') return 'text-red-300 bg-red-500/12 border-red-500/18'
45
- if (status === 'cancelled') return 'text-text-3 bg-white/[0.06] border-white/[0.08]'
46
- return 'text-sky-300 bg-sky-500/12 border-sky-500/18'
47
- }
48
-
49
- function phaseTone(phase: MissionPhase): string {
50
- if (phase === 'completed') return 'text-emerald-300'
51
- if (phase === 'failed') return 'text-red-300'
52
- if (phase === 'waiting') return 'text-amber-300'
53
- if (phase === 'verifying') return 'text-violet-300'
54
- if (phase === 'dispatching') return 'text-sky-300'
55
- return 'text-text-2'
56
- }
57
-
58
- function sourceLabel(mission: MissionSummary | Mission): string {
59
- const kind = mission.sourceRef?.kind || mission.source
60
- return kind.replace(/_/g, ' ')
61
- }
62
-
63
- async function postMissionAction(
64
- missionId: string,
65
- body: {
66
- action: 'resume' | 'replan' | 'cancel' | 'retry_verification' | 'wait'
67
- reason?: string
68
- waitKind?: MissionWaitKind
69
- untilAt?: number | null
70
- },
71
- ): Promise<void> {
72
- await api('POST', `/missions/${missionId}/actions`, body)
73
- }
74
-
75
- function missionRouteId(pathname: string): string | null {
76
- const parts = pathname.split('/').filter(Boolean)
77
- if (parts[0] !== 'missions') return null
78
- return parts[1] ? decodeURIComponent(parts[1]) : null
79
- }
80
-
81
- export default function MissionsPage() {
82
- const router = useRouter()
83
- const pathname = usePathname()
84
- const searchParams = useSearchParams()
85
- const missionHumanLoopEnabled = useAppStore((state) => state.appSettings.missionHumanLoopEnabled === true)
86
- const loadSettings = useAppStore((state) => state.loadSettings)
87
- const [missions, setMissions] = useState<Mission[]>([])
88
- const [selectedMissionId, setSelectedMissionId] = useState<string | null>(null)
89
- const [selectedMission, setSelectedMission] = useState<MissionDetailResponse | null>(null)
90
- const [statusFilter, setStatusFilter] = useState<MissionStatusFilter>('all')
91
- const [search, setSearch] = useState('')
92
- const [loading, setLoading] = useState(true)
93
- const [detailLoading, setDetailLoading] = useState(false)
94
- const [error, setError] = useState<string | null>(null)
95
- const [actionError, setActionError] = useState<string | null>(null)
96
- const [pendingAction, setPendingAction] = useState<string | null>(null)
97
- const [policyPending, setPolicyPending] = useState(false)
98
- const [structuredSessionOpen, setStructuredSessionOpen] = useState(false)
99
- const [linkedRun, setLinkedRun] = useState<{ id: string; status: string } | null>(null)
100
- const [waitReason, setWaitReason] = useState('')
101
- const [waitKind, setWaitKind] = useState<MissionWaitKind>('other')
102
- const [waitUntil, setWaitUntil] = useState('')
103
- const requestedMissionId = missionRouteId(pathname) || searchParams.get('missionId')
104
-
105
- const loadList = useCallback(async () => {
106
- try {
107
- const missionList = await api<Mission[]>('GET', '/missions?limit=120')
108
- const normalized = Array.isArray(missionList) ? missionList : []
109
- setMissions(normalized)
110
- setSelectedMissionId((current) => {
111
- if (requestedMissionId && normalized.some((mission) => mission.id === requestedMissionId)) {
112
- return requestedMissionId
113
- }
114
- if (current && normalized.some((mission) => mission.id === current)) return current
115
- return normalized[0]?.id || null
116
- })
117
- setError(null)
118
- } catch (err) {
119
- setError(err instanceof Error ? err.message : 'Unable to load missions.')
120
- } finally {
121
- setLoading(false)
122
- }
123
- }, [requestedMissionId])
124
-
125
- const loadDetail = useCallback(async (missionId: string | null) => {
126
- if (!missionId) {
127
- setSelectedMission(null)
128
- return
129
- }
130
- setDetailLoading(true)
131
- try {
132
- const detail = await api<MissionDetailResponse>('GET', `/missions/${missionId}`)
133
- setSelectedMission(detail)
134
- setActionError(null)
135
- } catch (err) {
136
- setActionError(err instanceof Error ? err.message : 'Unable to load mission detail.')
137
- setSelectedMission(null)
138
- } finally {
139
- setDetailLoading(false)
140
- }
141
- }, [])
142
-
143
- useEffect(() => {
144
- void loadList()
145
- }, [loadList])
146
-
147
- useEffect(() => {
148
- void loadSettings()
149
- }, [loadSettings])
150
-
151
- useEffect(() => {
152
- void loadDetail(selectedMissionId)
153
- }, [loadDetail, selectedMissionId])
154
-
155
- useWs('missions', loadList, 2000)
156
-
157
- const refreshLinkedRun = useCallback(() => {
158
- if (!selectedMissionId) {
159
- setLinkedRun(null)
160
- return
161
- }
162
- void api<Array<{ id: string; status: string }>>('GET', `/protocols/runs?missionId=${encodeURIComponent(selectedMissionId)}&limit=6`)
163
- .then((runs) => {
164
- const active = (Array.isArray(runs) ? runs : []).find((run) => !['completed', 'failed', 'cancelled', 'archived'].includes(run.status))
165
- setLinkedRun(active ? { id: active.id, status: active.status } : null)
166
- })
167
- .catch(() => setLinkedRun(null))
168
- }, [selectedMissionId])
169
-
170
- useEffect(() => {
171
- void refreshLinkedRun()
172
- }, [refreshLinkedRun])
173
-
174
- useWs(selectedMissionId ? 'protocol_runs' : '', refreshLinkedRun, 2000)
175
-
176
- const filtered = useMemo(() => {
177
- const normalizedSearch = search.trim().toLowerCase()
178
- return missions.filter((mission) => {
179
- if (statusFilter !== 'all' && mission.status !== statusFilter) return false
180
- if (!normalizedSearch) return true
181
- return [
182
- mission.objective,
183
- mission.currentStep,
184
- mission.waitState?.reason,
185
- mission.plannerSummary,
186
- mission.verifierSummary,
187
- ].some((value) => typeof value === 'string' && value.toLowerCase().includes(normalizedSearch))
188
- })
189
- }, [missions, search, statusFilter])
190
-
191
- const stats = useMemo(() => ({
192
- active: missions.filter((mission) => mission.status === 'active').length,
193
- waiting: missions.filter((mission) => mission.status === 'waiting').length,
194
- failed: missions.filter((mission) => mission.status === 'failed').length,
195
- completed: missions.filter((mission) => mission.status === 'completed').length,
196
- }), [missions])
197
-
198
- const handleAction = useCallback(async (action: 'resume' | 'replan' | 'cancel' | 'retry_verification') => {
199
- if (!selectedMission?.mission.id) return
200
- setPendingAction(action)
201
- try {
202
- await postMissionAction(selectedMission.mission.id, { action })
203
- await Promise.all([loadList(), loadDetail(selectedMission.mission.id)])
204
- setActionError(null)
205
- } catch (err) {
206
- setActionError(err instanceof Error ? err.message : 'Unable to update mission.')
207
- } finally {
208
- setPendingAction(null)
209
- }
210
- }, [loadDetail, loadList, selectedMission])
211
-
212
- const handleWaitAction = useCallback(async () => {
213
- if (!selectedMission?.mission.id) return
214
- const reason = waitReason.trim()
215
- if (!reason) {
216
- setActionError('A wait reason is required.')
217
- return
218
- }
219
- setPendingAction('wait')
220
- try {
221
- await postMissionAction(selectedMission.mission.id, {
222
- action: 'wait',
223
- reason,
224
- waitKind,
225
- untilAt: waitUntil ? Date.parse(waitUntil) : null,
226
- })
227
- await Promise.all([loadList(), loadDetail(selectedMission.mission.id)])
228
- setActionError(null)
229
- } catch (err) {
230
- setActionError(err instanceof Error ? err.message : 'Unable to update mission.')
231
- } finally {
232
- setPendingAction(null)
233
- }
234
- }, [loadDetail, loadList, selectedMission, waitKind, waitReason, waitUntil])
235
-
236
- const handleMissionHumanLoopToggle = useCallback(async () => {
237
- setPolicyPending(true)
238
- try {
239
- await api('PUT', '/settings', {
240
- missionHumanLoopEnabled: !missionHumanLoopEnabled,
241
- })
242
- await loadSettings()
243
- setActionError(null)
244
- } catch (err) {
245
- setActionError(err instanceof Error ? err.message : 'Unable to update mission policy.')
246
- } finally {
247
- setPolicyPending(false)
248
- }
249
- }, [loadSettings, missionHumanLoopEnabled])
250
-
251
- return (
252
- <div className="flex-1 min-h-0 overflow-y-auto bg-app px-4 py-5 md:px-6 md:py-6">
253
- <div className="mx-auto max-w-[1600px] space-y-5">
254
- <section className="rounded-[24px] border border-white/[0.06] bg-white/[0.03] p-5 md:p-6">
255
- <div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
256
- <div className="max-w-[760px]">
257
- <div className="inline-flex rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1 text-[11px] font-700 uppercase tracking-[0.18em] text-text-3/70">
258
- Mission Control
259
- </div>
260
- <h1 className="mt-4 font-display text-[34px] font-700 tracking-[-0.03em] text-text">Durable Objectives</h1>
261
- <p className="mt-3 max-w-[720px] text-[15px] leading-relaxed text-text-3/72">
262
- Inspect the agent&apos;s active objectives, blocked work, delegated branches, verification state, and queued follow-ups in one place.
263
- </p>
264
- </div>
265
- <div className="w-full max-w-[360px] rounded-[20px] border border-white/[0.06] bg-surface/70 p-4">
266
- <div className="text-[11px] font-700 uppercase tracking-[0.12em] text-text-3/55">List Filters</div>
267
- <input
268
- value={search}
269
- onChange={(event) => setSearch(event.target.value)}
270
- placeholder="Search objective, wait reason, or step"
271
- className="mt-3 w-full rounded-[12px] border border-white/[0.06] bg-white/[0.04] px-3 py-2.5 text-[14px] text-text outline-none placeholder:text-text-3/35"
272
- />
273
- <div className="mt-3 flex flex-wrap gap-2">
274
- {(['all', 'active', 'waiting', 'failed', 'completed'] as MissionStatusFilter[]).map((filter) => (
275
- <FilterPill
276
- key={filter}
277
- label={filter === 'all' ? 'All' : filter}
278
- active={statusFilter === filter}
279
- onClick={() => setStatusFilter(filter)}
280
- />
281
- ))}
282
- </div>
283
- <div className="mt-4 rounded-[14px] border border-white/[0.06] bg-white/[0.03] p-3">
284
- <div className="flex items-start justify-between gap-3">
285
- <div>
286
- <div className="text-[12px] font-700 text-text-2">Mission human loop</div>
287
- <div className="mt-1 text-[12px] leading-relaxed text-text-3/68">
288
- {missionHumanLoopEnabled
289
- ? 'Missions may stay open and wait for a human follow-up.'
290
- : 'Off by default. Generic “waiting for your next instruction” handoffs close instead of lingering as open missions.'}
291
- </div>
292
- <div className="mt-2 text-[11px] leading-relaxed text-text-3/50">
293
- Explicit tool approvals and real external blockers still apply.
294
- </div>
295
- </div>
296
- <button
297
- type="button"
298
- onClick={() => void handleMissionHumanLoopToggle()}
299
- disabled={policyPending}
300
- aria-pressed={missionHumanLoopEnabled}
301
- aria-label={missionHumanLoopEnabled ? 'Disable mission human loop' : 'Enable mission human loop'}
302
- className={`relative h-6 w-11 shrink-0 rounded-full transition-colors ${missionHumanLoopEnabled ? 'bg-accent-bright/80' : 'bg-white/[0.12]'} ${policyPending ? 'opacity-60' : ''}`}
303
- >
304
- <span
305
- className={`absolute left-[3px] top-[3px] h-[18px] w-[18px] rounded-full bg-white transition-transform ${missionHumanLoopEnabled ? 'translate-x-[20px]' : 'translate-x-0'}`}
306
- />
307
- </button>
308
- </div>
309
- </div>
310
- </div>
311
- </div>
312
- <div className="mt-5 grid grid-cols-2 gap-3 lg:grid-cols-4">
313
- <StatCard label="Active" value={stats.active} index={0} className="bg-surface-2 border-white/[0.05]" />
314
- <StatCard label="Waiting" value={stats.waiting} index={1} className="bg-surface-2 border-white/[0.05]" />
315
- <StatCard label="Failed" value={stats.failed} index={2} className="bg-surface-2 border-white/[0.05]" />
316
- <StatCard label="Completed" value={stats.completed} index={3} className="bg-surface-2 border-white/[0.05]" />
317
- </div>
318
- </section>
319
-
320
- {error && (
321
- <div className="rounded-[16px] border border-red-500/18 bg-red-500/8 px-4 py-3 text-[13px] text-red-200">
322
- {error}
323
- </div>
324
- )}
325
-
326
- <section className="grid gap-5 xl:grid-cols-[420px_minmax(0,1fr)]">
327
- <div className="flex min-h-0 flex-col rounded-[22px] border border-white/[0.06] bg-white/[0.02] p-3">
328
- <div className="flex items-center justify-between px-2 pb-2">
329
- <div>
330
- <div className="text-[12px] font-700 uppercase tracking-[0.1em] text-text-3/55">Missions</div>
331
- <div className="text-[12px] text-text-3/45">{filtered.length} visible</div>
332
- </div>
333
- </div>
334
- <div className="max-h-[70vh] min-h-0 space-y-2 overflow-y-auto pr-1">
335
- {loading ? (
336
- <PageLoader label="Loading missions..." />
337
- ) : filtered.length === 0 ? (
338
- <div className="px-3 py-4 text-[13px] text-text-3/55">No missions match the current filters.</div>
339
- ) : filtered.map((mission) => {
340
- const selected = mission.id === selectedMissionId
341
- return (
342
- <button
343
- key={mission.id}
344
- type="button"
345
- onClick={() => {
346
- setSelectedMissionId(mission.id)
347
- router.push(getMissionPath(mission.id))
348
- }}
349
- className={`w-full rounded-[18px] border px-4 py-3 text-left transition-all cursor-pointer ${
350
- selected
351
- ? 'border-accent-bright/30 bg-accent-bright/10'
352
- : 'border-white/[0.06] bg-surface/70 hover:bg-white/[0.04]'
353
- }`}
354
- >
355
- <div className="flex items-start justify-between gap-3">
356
- <div className="min-w-0">
357
- <div className="line-clamp-2 text-[14px] font-700 text-text">{mission.objective}</div>
358
- <div className="mt-2 flex flex-wrap gap-1.5">
359
- <span className={`rounded-full border px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] ${missionStatusTone(mission.status)}`}>
360
- {mission.status}
361
- </span>
362
- <span className="rounded-full border border-white/[0.08] px-2 py-0.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/70">
363
- {sourceLabel(mission)}
364
- </span>
365
- </div>
366
- </div>
367
- <div className="shrink-0 text-[11px] text-text-3/45">{timeAgo(mission.updatedAt)}</div>
368
- </div>
369
- <div className={`mt-3 text-[12px] font-600 ${phaseTone(mission.phase)}`}>{mission.phase}</div>
370
- <div className="mt-1 text-[12px] leading-relaxed text-text-3/72 line-clamp-2">
371
- {mission.waitState?.reason || mission.currentStep || mission.verifierSummary || mission.plannerSummary || 'Mission active.'}
372
- </div>
373
- <div className="mt-3 flex gap-3 text-[11px] text-text-3/45">
374
- <span>{mission.taskIds?.length || 0} tasks</span>
375
- <span>{mission.childMissionIds?.length || 0} child missions</span>
376
- </div>
377
- </button>
378
- )
379
- })}
380
- </div>
381
- </div>
382
-
383
- <div className="rounded-[22px] border border-white/[0.06] bg-white/[0.02] p-4 md:p-5">
384
- {!selectedMissionId ? (
385
- <div className="rounded-[18px] border border-dashed border-white/[0.08] px-5 py-8 text-[14px] text-text-3/60">
386
- Select a mission to inspect its detail, linked work, and operator actions.
387
- </div>
388
- ) : detailLoading && !selectedMission ? (
389
- <PageLoader label="Loading mission..." />
390
- ) : selectedMission ? (
391
- <div className="space-y-4">
392
- <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
393
- <div className="min-w-0">
394
- <div className="flex flex-wrap gap-2">
395
- <span className={`rounded-full border px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] ${missionStatusTone(selectedMission.mission.status)}`}>
396
- {selectedMission.mission.status}
397
- </span>
398
- <span className="rounded-full border border-white/[0.08] px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] text-text-3/70">
399
- {sourceLabel(selectedMission.mission)}
400
- </span>
401
- <span className={`rounded-full border border-white/[0.08] px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] ${phaseTone(selectedMission.mission.phase)}`}>
402
- {selectedMission.mission.phase}
403
- </span>
404
- </div>
405
- <h2 className="mt-3 font-display text-[28px] font-700 tracking-[-0.03em] text-text">
406
- {selectedMission.mission.objective}
407
- </h2>
408
- <p className="mt-2 text-[13px] text-text-3/58">
409
- Updated {timeAgo(selectedMission.mission.updatedAt)}
410
- {selectedMission.parent ? ` · child of ${selectedMission.parent.objective}` : ''}
411
- </p>
412
- </div>
413
- <div className="flex flex-wrap gap-2">
414
- {linkedRun && (
415
- <button
416
- type="button"
417
- onClick={() => router.push(`/protocols?runId=${encodeURIComponent(linkedRun.id)}`)}
418
- className="rounded-[12px] border border-sky-500/20 bg-sky-500/10 px-3 py-2 text-[12px] font-700 text-sky-100 transition-colors hover:bg-sky-500/16"
419
- >
420
- Open Session
421
- </button>
422
- )}
423
- <button
424
- type="button"
425
- onClick={() => setStructuredSessionOpen(true)}
426
- className="rounded-[12px] bg-accent-bright px-3 py-2 text-[12px] font-700 text-black transition-colors hover:opacity-90"
427
- >
428
- {linkedRun ? 'Run Another Structured Session' : 'Run Structured Session'}
429
- </button>
430
- {([
431
- ['resume', 'Resume'],
432
- ['replan', 'Replan'],
433
- ['retry_verification', 'Retry Verification'],
434
- ['cancel', 'Cancel'],
435
- ] as const).map(([action, label]) => (
436
- <button
437
- key={action}
438
- type="button"
439
- onClick={() => void handleAction(action)}
440
- disabled={pendingAction !== null}
441
- className="rounded-[12px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-700 text-text-2 transition-colors hover:bg-white/[0.07] disabled:cursor-not-allowed disabled:opacity-50"
442
- >
443
- {pendingAction === action ? 'Working…' : label}
444
- </button>
445
- ))}
446
- </div>
447
- </div>
448
-
449
- {actionError && (
450
- <div className="rounded-[16px] border border-red-500/18 bg-red-500/8 px-4 py-3 text-[13px] text-red-200">
451
- {actionError}
452
- </div>
453
- )}
454
-
455
- <div className="grid gap-4 xl:grid-cols-[minmax(0,1.3fr)_minmax(0,0.9fr)]">
456
- <div className="space-y-4">
457
- {(selectedMission.parent || selectedMission.children.length > 0) && (
458
- <div className="rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
459
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Mission Graph</div>
460
- <div className="mt-3 space-y-3">
461
- {selectedMission.parent && (
462
- <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
463
- <div className="text-[11px] uppercase tracking-[0.08em] text-text-3/45">Parent</div>
464
- <div className="mt-1 flex items-start justify-between gap-3">
465
- <div>
466
- <div className="text-[12px] font-700 text-text">{selectedMission.parent.objective}</div>
467
- <div className="mt-1 text-[11px] text-text-3/55">{selectedMission.parent.status} · {selectedMission.parent.phase}</div>
468
- </div>
469
- <button
470
- type="button"
471
- onClick={() => {
472
- setSelectedMissionId(selectedMission.parent?.id || null)
473
- if (selectedMission.parent?.id) router.push(getMissionPath(selectedMission.parent.id))
474
- }}
475
- className="rounded-[10px] border border-white/[0.08] bg-transparent px-2.5 py-1 text-[11px] font-700 text-text-2 transition-colors hover:bg-white/[0.05]"
476
- >
477
- Open
478
- </button>
479
- </div>
480
- </div>
481
- )}
482
- {selectedMission.children.length > 0 && (
483
- <div>
484
- <div className="mb-2 text-[12px] font-700 text-text-2">Children</div>
485
- <div className="space-y-2">
486
- {selectedMission.children.map((child) => (
487
- <div key={child.id} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
488
- <div className="flex items-start justify-between gap-3">
489
- <div>
490
- <div className="text-[12px] font-700 text-text">{child.objective}</div>
491
- <div className="mt-1 text-[11px] text-text-3/55">{child.status} · {child.phase}</div>
492
- </div>
493
- <button
494
- type="button"
495
- onClick={() => {
496
- setSelectedMissionId(child.id)
497
- router.push(getMissionPath(child.id))
498
- }}
499
- className="rounded-[10px] border border-white/[0.08] bg-transparent px-2.5 py-1 text-[11px] font-700 text-text-2 transition-colors hover:bg-white/[0.05]"
500
- >
501
- Open
502
- </button>
503
- </div>
504
- </div>
505
- ))}
506
- </div>
507
- </div>
508
- )}
509
- </div>
510
- </div>
511
- )}
512
-
513
- <div className="rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
514
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Current Lane</div>
515
- <div className="mt-3 space-y-2 text-[13px] text-text-2">
516
- <div><span className="text-text-3/55">Current step:</span> {selectedMission.mission.currentStep || 'Not set'}</div>
517
- <div><span className="text-text-3/55">Planner summary:</span> {selectedMission.mission.plannerSummary || 'None recorded'}</div>
518
- <div><span className="text-text-3/55">Verifier summary:</span> {selectedMission.mission.verifierSummary || 'None recorded'}</div>
519
- </div>
520
- </div>
521
-
522
- <div className="rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
523
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Linked Work</div>
524
- <div className="mt-3 grid gap-3 md:grid-cols-2">
525
- <div>
526
- <div className="mb-2 text-[12px] font-700 text-text-2">Tasks</div>
527
- <div className="space-y-2">
528
- {selectedMission.linkedTasks.length === 0 ? (
529
- <div className="text-[12px] text-text-3/55">No linked tasks.</div>
530
- ) : selectedMission.linkedTasks.map((task) => (
531
- <div key={task.id} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
532
- <div className="text-[12px] font-700 text-text">{task.title}</div>
533
- <div className="mt-1 text-[11px] text-text-3/55">{task.status}</div>
534
- </div>
535
- ))}
536
- </div>
537
- </div>
538
- <div>
539
- <div className="mb-2 text-[12px] font-700 text-text-2">Queued Turns</div>
540
- <div className="space-y-2">
541
- {selectedMission.queuedTurns.length === 0 ? (
542
- <div className="text-[12px] text-text-3/55">No queued turns.</div>
543
- ) : selectedMission.queuedTurns.map((turn) => (
544
- <div key={turn.runId} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
545
- <div className="line-clamp-2 text-[12px] text-text">{turn.text || '(attachment only)'}</div>
546
- <div className="mt-1 text-[11px] text-text-3/55">Position {turn.position} · {timeAgo(turn.queuedAt)}</div>
547
- </div>
548
- ))}
549
- </div>
550
- </div>
551
- </div>
552
- </div>
553
-
554
- <div className="rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
555
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Recent Runs</div>
556
- <div className="mt-3 space-y-2">
557
- {selectedMission.recentRuns.length === 0 ? (
558
- <div className="text-[12px] text-text-3/55">No linked runs yet.</div>
559
- ) : selectedMission.recentRuns.map((run) => (
560
- <div key={run.id} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
561
- <div className="flex items-center justify-between gap-3">
562
- <div className="text-[12px] font-700 text-text">{run.status}</div>
563
- <div className="text-[11px] text-text-3/45">{timeAgo(run.queuedAt)}</div>
564
- </div>
565
- <div className="mt-1 line-clamp-2 text-[12px] text-text-3/68">{run.messagePreview || run.resultPreview || run.error || 'No summary recorded.'}</div>
566
- </div>
567
- ))}
568
- </div>
569
- </div>
570
- </div>
571
-
572
- <div className="space-y-4">
573
- <div className="rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
574
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Wait and Verification</div>
575
- <div className="mt-3 space-y-2 text-[13px] text-text-2">
576
- <div><span className="text-text-3/55">Waiting reason:</span> {selectedMission.mission.waitState?.reason || 'Not waiting'}</div>
577
- <div><span className="text-text-3/55">Wait kind:</span> {selectedMission.mission.waitState?.kind || 'None'}</div>
578
- <div><span className="text-text-3/55">Verification candidate:</span> {selectedMission.mission.verificationState?.candidate ? 'Yes' : 'No'}</div>
579
- <div><span className="text-text-3/55">Evidence:</span> {selectedMission.mission.verificationState?.evidenceSummary || 'No evidence summary yet'}</div>
580
- </div>
581
- <div className="mt-4 rounded-[14px] border border-white/[0.06] bg-white/[0.03] p-3">
582
- <div className="text-[12px] font-700 text-text-2">Mark Waiting</div>
583
- <div className="mt-3 space-y-2">
584
- <select
585
- value={waitKind}
586
- onChange={(event) => setWaitKind(event.target.value as typeof waitKind)}
587
- className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.04] px-3 py-2 text-[13px] text-text outline-none"
588
- >
589
- {(['human_reply', 'approval', 'external_dependency', 'provider', 'blocked_task', 'blocked_mission', 'scheduled', 'other'] as const).map((kind) => (
590
- <option key={kind} value={kind}>{kind.replace(/_/g, ' ')}</option>
591
- ))}
592
- </select>
593
- <textarea
594
- value={waitReason}
595
- onChange={(event) => setWaitReason(event.target.value)}
596
- placeholder="Why should this mission wait?"
597
- rows={3}
598
- className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.04] px-3 py-2 text-[13px] text-text outline-none placeholder:text-text-3/35"
599
- />
600
- <input
601
- type="datetime-local"
602
- value={waitUntil}
603
- onChange={(event) => setWaitUntil(event.target.value)}
604
- className="w-full rounded-[12px] border border-white/[0.06] bg-white/[0.04] px-3 py-2 text-[13px] text-text outline-none"
605
- />
606
- <button
607
- type="button"
608
- onClick={() => void handleWaitAction()}
609
- disabled={pendingAction !== null}
610
- className="rounded-[12px] border border-white/[0.08] bg-white/[0.04] px-3 py-2 text-[12px] font-700 text-text-2 transition-colors hover:bg-white/[0.07] disabled:cursor-not-allowed disabled:opacity-50"
611
- >
612
- {pendingAction === 'wait' ? 'Working…' : 'Set Wait State'}
613
- </button>
614
- </div>
615
- </div>
616
- {selectedMission.approvals.length > 0 && (
617
- <div className="mt-4">
618
- <div className="mb-2 text-[12px] font-700 text-text-2">Approvals</div>
619
- <div className="space-y-2">
620
- {selectedMission.approvals.slice(0, 4).map((approval) => (
621
- <div key={approval.id} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
622
- <div className="text-[12px] font-700 text-text">{approval.status}</div>
623
- <div className="mt-1 text-[11px] text-text-3/55">{approval.title || approval.id}</div>
624
- </div>
625
- ))}
626
- </div>
627
- </div>
628
- )}
629
- </div>
630
-
631
- <div className="flex min-h-0 flex-col rounded-[18px] border border-white/[0.06] bg-surface/70 p-4">
632
- <div className="text-[11px] font-700 uppercase tracking-[0.1em] text-text-3/55">Timeline</div>
633
- <div className="mt-3 max-h-[420px] min-h-0 space-y-2 overflow-y-auto pr-1">
634
- {selectedMission.events.length === 0 ? (
635
- <div className="text-[12px] text-text-3/55">No mission events recorded yet.</div>
636
- ) : selectedMission.events.map((event) => (
637
- <div key={event.id} className="rounded-[14px] border border-white/[0.06] bg-white/[0.03] px-3 py-2">
638
- <div className="flex items-center justify-between gap-3">
639
- <div className="text-[12px] font-700 text-text">{event.summary}</div>
640
- <div className="text-[11px] text-text-3/45">{timeAgo(event.createdAt)}</div>
641
- </div>
642
- <div className="mt-1 text-[11px] uppercase tracking-[0.08em] text-text-3/45">
643
- {event.type} · {event.source}
644
- </div>
645
- </div>
646
- ))}
647
- <a
648
- href={`/api/missions/${selectedMission.mission.id}/events?limit=200`}
649
- target="_blank"
650
- rel="noreferrer"
651
- className="inline-flex text-[12px] font-700 text-accent-bright hover:underline"
652
- >
653
- Open raw event stream
654
- </a>
655
- </div>
656
- </div>
657
- </div>
658
- </div>
659
- </div>
660
- ) : (
661
- <div className="rounded-[18px] border border-dashed border-white/[0.08] px-5 py-8 text-[14px] text-text-3/60">
662
- Select a mission to inspect it.
663
- </div>
664
- )}
665
- </div>
666
- </section>
667
- </div>
668
- <StructuredSessionLauncher
669
- open={structuredSessionOpen}
670
- onClose={() => setStructuredSessionOpen(false)}
671
- onCreated={(run) => {
672
- router.push(`/protocols?runId=${encodeURIComponent(run.id)}`)
673
- }}
674
- initialContext={{
675
- missionId: selectedMission?.mission.id || null,
676
- missionLabel: selectedMission?.mission.objective || null,
677
- participantAgentIds: selectedMission?.mission.agentId ? [selectedMission.mission.agentId] : [],
678
- facilitatorAgentId: selectedMission?.mission.agentId || null,
679
- title: selectedMission ? `Structured session: ${selectedMission.mission.objective}` : null,
680
- goal: selectedMission?.mission.objective || null,
681
- }}
682
- />
683
- </div>
684
- )
685
- }