@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/heartbeat-defaults'
3
4
  import { useEffect, useState, useMemo, useRef, useCallback, type ReactNode } from 'react'
4
5
  import type { Session } from '@/types'
5
6
  import { useAppStore } from '@/stores/use-app-store'
@@ -18,6 +19,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
18
19
  import { toast } from 'sonner'
19
20
  import type { ProviderType } from '@/types'
20
21
  import { copyTextToClipboard } from '@/lib/clipboard'
22
+ import { buildOpenClawMainSessionKey } from '@/lib/openclaw-agent-id'
21
23
  import { useWs } from '@/hooks/use-ws'
22
24
 
23
25
  function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
@@ -32,6 +34,40 @@ function Tip({ label, children, side = 'bottom' }: { label: string; children: Re
32
34
  )
33
35
  }
34
36
 
37
+ function HeaderChip({
38
+ children,
39
+ title,
40
+ onClick,
41
+ className = '',
42
+ active = false,
43
+ }: {
44
+ children: ReactNode
45
+ title?: string
46
+ onClick?: () => void
47
+ className?: string
48
+ active?: boolean
49
+ }) {
50
+ const baseClass = `inline-flex max-w-full items-center gap-1.5 rounded-[9px] border px-2.5 py-1 text-[10px] font-600 backdrop-blur-sm transition-colors ${
51
+ active
52
+ ? 'border-accent-bright/20 bg-accent-soft/50 text-accent-bright'
53
+ : 'border-white/[0.06] bg-white/[0.03] text-text-3/68'
54
+ } ${onClick ? 'cursor-pointer hover:border-white/[0.1] hover:bg-white/[0.06] hover:text-text-2' : ''} ${className}`
55
+
56
+ if (onClick) {
57
+ return (
58
+ <button type="button" onClick={onClick} title={title} className={baseClass}>
59
+ {children}
60
+ </button>
61
+ )
62
+ }
63
+
64
+ return (
65
+ <span title={title} className={baseClass}>
66
+ {children}
67
+ </span>
68
+ )
69
+ }
70
+
35
71
  function shortPath(p: string): string {
36
72
  return (p || '').replace(/^\/Users\/\w+/, '~')
37
73
  }
@@ -78,8 +114,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
78
114
  const toggleTts = useChatStore((s) => s.toggleTts)
79
115
  const soundEnabled = useChatStore((s) => s.soundEnabled)
80
116
  const toggleSound = useChatStore((s) => s.toggleSound)
81
- const debugOpen = useChatStore((s) => s.debugOpen)
82
- const setDebugOpen = useChatStore((s) => s.setDebugOpen)
83
117
  const agentStatus = useChatStore((s) => s.agentStatus)
84
118
  const agents = useAppStore((s) => s.agents)
85
119
  const tasks = useAppStore((s) => s.tasks)
@@ -100,6 +134,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
100
134
  const providers = useAppStore((s) => s.providers)
101
135
  const loadProviders = useAppStore((s) => s.loadProviders)
102
136
  const modelName = session.model || agent?.model || ''
137
+ const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
103
138
  const [modelSwitcherOpen, setModelSwitcherOpen] = useState(false)
104
139
  const modelSwitcherRef = useRef<HTMLDivElement>(null)
105
140
  const [copied, setCopied] = useState(false)
@@ -108,9 +143,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
108
143
  const hbDropdownRef = useRef<HTMLDivElement>(null)
109
144
  const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
110
145
  const sourceDropdownRef = useRef<HTMLDivElement>(null)
111
- const [mainLoopSaving, setMainLoopSaving] = useState(false)
112
- const [mainLoopError, setMainLoopError] = useState('')
113
- const [mainLoopNotice, setMainLoopNotice] = useState('')
114
146
  const [syncingHistory, setSyncingHistory] = useState(false)
115
147
  const [syncResult, setSyncResult] = useState('')
116
148
  const [renaming, setRenaming] = useState(false)
@@ -122,9 +154,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
122
154
  const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
123
155
  const [walletBalance, setWalletBalance] = useState<number | null>(null)
124
156
  const [headerWidgets, setHeaderWidgets] = useState<Array<{ id: string; label: string; icon?: string }>>([])
125
- const [goalModalOpen, setGoalModalOpen] = useState(false)
126
- const [goalDraft, setGoalDraft] = useState('')
127
- const goalInputRef = useRef<HTMLTextAreaElement>(null)
128
157
 
129
158
  useEffect(() => {
130
159
  api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
@@ -150,6 +179,46 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
150
179
  }, [fetchWalletBalance])
151
180
  useWs('wallets', fetchWalletBalance)
152
181
 
182
+ const workspaceLabel = useMemo(() => shortPath(session.cwd), [session.cwd])
183
+ const liveStatus = agentStatus || null
184
+ const threadContextLabel = useMemo(() => {
185
+ const title = session.connectorContext?.threadTitle?.trim()
186
+ if (title) return title
187
+ const persona = session.connectorContext?.threadPersonaLabel?.trim()
188
+ if (persona) return persona
189
+ return null
190
+ }, [session.connectorContext?.threadPersonaLabel, session.connectorContext?.threadTitle])
191
+ const connectorPresenceMeta = useMemo(() => {
192
+ if (!connector) return null
193
+ const lastAt = connectorPresence?.lastMessageAt
194
+ if (!lastAt) {
195
+ return {
196
+ label: 'Idle',
197
+ dotClass: 'bg-text-3/30',
198
+ textClass: 'text-text-3/45',
199
+ }
200
+ }
201
+ const ago = Date.now() - lastAt
202
+ if (ago < 5 * 60_000) {
203
+ return {
204
+ label: 'Active',
205
+ dotClass: 'bg-emerald-400',
206
+ textClass: 'text-emerald-400',
207
+ }
208
+ }
209
+ if (ago < 30 * 60_000) {
210
+ return {
211
+ label: `${Math.floor(ago / 60_000)}m ago`,
212
+ dotClass: 'bg-amber-400',
213
+ textClass: 'text-amber-300',
214
+ }
215
+ }
216
+ return {
217
+ label: 'Idle',
218
+ dotClass: 'bg-text-3/30',
219
+ textClass: 'text-text-3/45',
220
+ }
221
+ }, [connector, connectorPresence?.lastMessageAt])
153
222
 
154
223
  const visibleHeaderWidgets = useMemo(() => {
155
224
  const seen = new Set<string>()
@@ -161,6 +230,25 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
161
230
  })
162
231
  }, [headerWidgets])
163
232
 
233
+ const walletHeaderMeta = useMemo(() => {
234
+ if (!agent?.id) {
235
+ return {
236
+ label: 'Wallets',
237
+ title: 'Open wallets',
238
+ }
239
+ }
240
+ if (!agent.walletId) {
241
+ return {
242
+ label: 'Create wallet',
243
+ title: 'Create wallet',
244
+ }
245
+ }
246
+ return {
247
+ label: walletBalance !== null ? `${walletBalance.toFixed(3)} SOL` : 'Wallet',
248
+ title: 'View wallet',
249
+ }
250
+ }, [agent?.id, agent?.walletId, walletBalance])
251
+
164
252
  const handleHeaderWidgetClick = (widgetId: string) => {
165
253
  if (widgetId === 'wallet-status') {
166
254
  if (agent?.id) setWalletPanelAgentId(agent.id)
@@ -248,7 +336,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
248
336
  return null
249
337
  }
250
338
  // Global defaults
251
- let sec = resolveFrom(appSettings) ?? 1800
339
+ let sec = resolveFrom(appSettings) ?? DEFAULT_HEARTBEAT_INTERVAL_SEC
252
340
  let enabled = sec > 0
253
341
  let explicitOptIn = false
254
342
  // Agent layer
@@ -270,13 +358,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
270
358
  }
271
359
  }, [appSettings, agent, session])
272
360
  const heartbeatWillRun = heartbeatEnabled && (loopIsOngoing || heartbeatExplicitOptIn)
273
- const hasMainLoop = session.id.startsWith('agent-thread-') || session.sessionType === 'orchestrated'
274
- const missionState = session.mainLoopState || {}
275
- const missionPaused = missionState.paused === true
276
- const missionMode = missionState.autonomyMode === 'assist' ? 'assist' : 'autonomous'
277
- const missionStatus = missionState.status || 'idle'
278
- const missionMomentum = typeof missionState.momentumScore === 'number' ? missionState.momentumScore : null
279
- const missionEventsCount = missionState.pendingEvents?.length || 0
280
361
 
281
362
  const handleToggleHeartbeat = async () => {
282
363
  if (!heartbeatSupported || heartbeatSaving) return
@@ -322,68 +403,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
322
403
  }
323
404
  }
324
405
 
325
- const postMainLoopAction = async (action: string, extra?: Record<string, unknown>) => {
326
- if (!hasMainLoop || mainLoopSaving) return
327
- setMainLoopSaving(true)
328
- try {
329
- const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/chats/${session.id}/main-loop`, {
330
- action,
331
- ...(extra || {}),
332
- })
333
- setMainLoopError('')
334
- if (action === 'nudge') {
335
- setMainLoopNotice(result?.deduped ? 'Nudge already queued.' : 'Nudge queued.')
336
- } else if (action === 'set_mode') {
337
- setMainLoopNotice(`Mode set to ${extra?.mode === 'assist' ? 'Assist' : 'Auto'}.`)
338
- } else {
339
- setMainLoopNotice('')
340
- }
341
- await loadSessions()
342
- } catch (err: unknown) {
343
- const message = err instanceof Error ? err.message : 'Failed to update mission controls.'
344
- setMainLoopError(message)
345
- } finally {
346
- setMainLoopSaving(false)
347
- }
348
- }
349
-
350
- const handleToggleMissionPause = () => {
351
- void postMainLoopAction(missionPaused ? 'resume' : 'pause')
352
- }
353
-
354
- const handleToggleMissionMode = () => {
355
- const nextMode = missionMode === 'autonomous' ? 'assist' : 'autonomous'
356
- void postMainLoopAction('set_mode', { mode: nextMode })
357
- }
358
-
359
- const handleNudgeMission = () => {
360
- void postMainLoopAction('nudge')
361
- }
362
-
363
- const handleOpenGoalModal = () => {
364
- if (!hasMainLoop) return
365
- setGoalDraft(typeof missionState.goal === 'string' ? missionState.goal : '')
366
- setGoalModalOpen(true)
367
- requestAnimationFrame(() => goalInputRef.current?.focus())
368
- }
369
-
370
- const handleSubmitGoal = () => {
371
- const goal = goalDraft.trim()
372
- setGoalModalOpen(false)
373
- if (!goal) return
374
- void postMainLoopAction('set_goal', { goal })
375
- }
376
-
377
- const handleClearMissionEvents = () => {
378
- if (!hasMainLoop || missionEventsCount <= 0) return
379
- void postMainLoopAction('clear_events')
380
- }
381
-
382
406
  const isOpenClawAgent = agent?.provider === 'openclaw'
383
- // Derive OpenClaw session key: agent sessions use "agent:<name>:main" convention
384
- const openclawSessionKey = isOpenClawAgent && agent
385
- ? `agent:${agent.name.toLowerCase().replace(/\s+/g, '-')}:main`
386
- : null
407
+ const openclawSessionKey = isOpenClawAgent ? buildOpenClawMainSessionKey(agent?.name) : null
387
408
 
388
409
  const handleSyncHistory = async () => {
389
410
  if (!openclawSessionKey || syncingHistory) return
@@ -511,36 +532,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
511
532
  }, [session.name, loadConnectors])
512
533
 
513
534
  useEffect(() => {
514
- setMainLoopError('')
515
- setMainLoopNotice('')
516
535
  setModelSwitcherOpen(false)
517
536
  }, [session.id])
518
537
 
519
- useEffect(() => {
520
- if (!mainLoopNotice) return
521
- const timer = setTimeout(() => setMainLoopNotice(''), 2500)
522
- return () => clearTimeout(timer)
523
- }, [mainLoopNotice])
524
-
525
- // Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
538
+ // Context bar shows for tools, memories, source filter, task links, resume handles, browser
526
539
  const hasToolToggles = ((agent?.plugins?.length ?? 0) > 0) || ((session.plugins?.length ?? 0) > 0)
527
540
  const hasMemoryLink = !!(agent && session.plugins?.includes('memory'))
528
541
  const hasSourceFilter = !!hasMultipleSources
529
- const hasContextBar = !!(hasMainLoop || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
542
+ const hasContextBar = !!(hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
530
543
 
531
544
  return (
532
545
  <>
533
546
  <header
534
547
  className="relative z-20 border-b border-white/[0.06] shrink-0"
535
548
  style={{
536
- background: 'linear-gradient(180deg, rgba(var(--rgb-bg, 15,15,26), 0.95) 0%, rgba(var(--rgb-bg, 15,15,26), 0.88) 100%)',
549
+ background: 'radial-gradient(circle at top left, rgba(66, 211, 255, 0.08), transparent 32%), radial-gradient(circle at top right, rgba(255, 190, 92, 0.05), transparent 28%), linear-gradient(180deg, rgba(var(--rgb-bg, 15,15,26), 0.96) 0%, rgba(var(--rgb-bg, 15,15,26), 0.9) 100%)',
537
550
  backdropFilter: 'blur(20px) saturate(1.4)',
538
551
  WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
539
552
  ...(mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : {}),
540
553
  }}
541
554
  >
542
555
  {/* Main row */}
543
- <div className="flex items-center gap-2 px-3.5 py-1.5 min-h-[48px]">
556
+ <div className="flex flex-wrap items-start gap-3 px-4 py-2.5 min-h-[64px]">
544
557
  {/* Back button */}
545
558
  {onBack && (
546
559
  <IconButton onClick={onBack} aria-label="Go back" size="sm">
@@ -581,10 +594,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
581
594
  )}
582
595
 
583
596
  {/* Identity + metadata — fills center */}
584
- <div className="flex-1 min-w-0 flex items-center gap-3">
585
- {/* Name (row 1) + tools (row 2) */}
586
- <div className="flex flex-col gap-0.5 min-w-0 shrink">
587
- <div className="flex items-center gap-2 min-w-0">
597
+ <div className="min-w-0 flex-1">
598
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
588
599
  {renaming && agent ? (
589
600
  <span ref={renameContainerRef} className="inline-flex items-center gap-2">
590
601
  <input
@@ -602,99 +613,90 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
602
613
  {renameSaving && <span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-accent-bright animate-spin shrink-0" />}
603
614
  {renameError && <span className="text-[10px] text-red-400 shrink-0">{renameError}</span>}
604
615
  </span>
616
+ ) : agent ? (
617
+ <button
618
+ type="button"
619
+ onClick={startRename}
620
+ title="Rename agent"
621
+ className="group/title inline-flex min-w-0 items-center gap-1.5 rounded-[9px] px-1 py-0.5 text-left transition-colors hover:bg-white/[0.03] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent-bright/40"
622
+ >
623
+ <span className="font-display text-[16px] font-700 truncate tracking-[-0.02em] text-text transition-colors group-hover/title:text-accent-bright">
624
+ {(session.shortcutForAgentId && agent.id === session.shortcutForAgentId) || agent.threadSessionId === session.id
625
+ ? agent.name
626
+ : session.name}
627
+ </span>
628
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0 text-text-3/40 opacity-0 transition-opacity group-hover/title:opacity-100 group-focus-visible/title:opacity-100">
629
+ <path d="M12 20h9" />
630
+ <path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
631
+ </svg>
632
+ </button>
605
633
  ) : (
606
- <span
607
- className={`font-display text-[15px] font-700 truncate tracking-[-0.02em] text-text${agent ? ' cursor-pointer hover:text-accent-bright transition-colors duration-200' : ''}`}
608
- onClick={agent ? startRename : undefined}
609
- title={agent ? 'Click to rename' : undefined}
610
- >{
611
- session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
612
- : session.name
613
- }</span>
634
+ <span className="font-display text-[16px] font-700 truncate tracking-[-0.02em] text-text">{session.name}</span>
614
635
  )}
615
636
  {connector && connectorMeta && (
616
637
  <span
617
- className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[5px] border text-[9px] font-700 uppercase tracking-wider shrink-0"
638
+ className="inline-flex min-w-0 items-center gap-1 px-2 py-1 rounded-[8px] border text-[10px] font-700 uppercase tracking-wider shrink-0"
618
639
  style={{
619
640
  color: connectorMeta.color,
620
- backgroundColor: `${connectorMeta.color}10`,
621
- borderColor: `${connectorMeta.color}20`,
641
+ backgroundColor: `${connectorMeta.color}12`,
642
+ borderColor: `${connectorMeta.color}22`,
622
643
  }}
623
644
  title={`${connector.name} connector`}
624
645
  >
625
646
  <ConnectorPlatformIcon platform={connector.platform} size={10} />
626
- {connectorMeta.label}
647
+ <span className="truncate max-w-[140px]">{connectorMeta.label}</span>
627
648
  </span>
628
649
  )}
629
- {connector && connectorPresence && (() => {
630
- const lastAt = connectorPresence.lastMessageAt
631
- if (!lastAt) return (
632
- <span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-text-3/40">
633
- <span className="w-1.5 h-1.5 rounded-full bg-text-3/30" />
634
- Idle
635
- </span>
636
- )
637
- const ago = Date.now() - lastAt
638
- const isActive = ago < 5 * 60_000
639
- const isRecent = ago < 30 * 60_000
640
- const label = isActive ? 'Active' : isRecent ? `${Math.floor(ago / 60_000)}m ago` : 'Idle'
641
- const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/30'
642
- const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/40'
643
- return (
644
- <span className={`shrink-0 inline-flex items-center gap-1 text-[10px] ${textColor}`}>
645
- <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
646
- {label}
647
- </span>
648
- )
649
- })()}
650
- {agent?.isOrchestrator && (
651
- <span className="px-1.5 py-0.5 rounded-[5px] bg-amber-500/10 text-amber-500 text-[9px] font-700 uppercase tracking-wider shrink-0">Orch</span>
650
+ {connectorPresenceMeta && (
651
+ <HeaderChip className={`${connectorPresenceMeta.textClass} shrink-0`}>
652
+ <span className={`w-1.5 h-1.5 rounded-full ${connectorPresenceMeta.dotClass}`} />
653
+ {connectorPresenceMeta.label}
654
+ </HeaderChip>
655
+ )}
656
+ {agent?.platformAssignScope === 'all' && (
657
+ <HeaderChip className="bg-amber-500/10 border-amber-500/15 text-amber-400 shrink-0">Delegates</HeaderChip>
652
658
  )}
653
659
  {streaming && (
654
- <span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
660
+ <HeaderChip className="bg-accent-soft/60 border-accent-bright/20 text-accent-bright shrink-0">
661
+ <span className="w-1.5 h-1.5 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
662
+ Responding
663
+ </HeaderChip>
655
664
  )}
656
665
  </div>
657
- {hasToolToggles && <ChatToolToggles session={session} />}
658
- </div>
659
-
660
- {/* Metadata tray: wallet · model · path · status */}
661
- <div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
662
- <span className="text-text-3/10 text-[10px] select-none shrink-0">/</span>
666
+ <div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
667
+ {hasToolToggles && <ChatToolToggles session={session} />}
663
668
  {visibleHeaderWidgets.map((widget) => {
664
669
  const actionable = widget.id === 'wallet-status'
665
- const walletLabel = walletBalance !== null
666
- ? `${walletBalance.toFixed(3)} SOL`
670
+ const walletLabel = actionable
671
+ ? walletHeaderMeta.label
667
672
  : (widget.label || 'Wallet')
673
+ const widgetTitle = actionable
674
+ ? walletHeaderMeta.title
675
+ : widget.label
668
676
  return (
669
- <button
677
+ <HeaderChip
670
678
  key={widget.id}
671
- type="button"
672
679
  onClick={actionable ? () => handleHeaderWidgetClick(widget.id) : undefined}
673
- className={`inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] text-[11px] font-mono transition-colors ${
674
- actionable ? 'cursor-pointer text-text-3/45 hover:text-text-3/70 hover:bg-white/[0.04]' : 'cursor-default text-text-3/55'
675
- }`}
676
- title={actionable ? 'View wallet' : widget.label}
680
+ title={widgetTitle}
681
+ className={actionable ? 'text-text-3/80' : ''}
677
682
  >
678
683
  {actionable ? (
679
684
  <>
680
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
685
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
681
686
  <rect x="2" y="6" width="20" height="14" rx="2" />
682
687
  <path d="M22 10H18a2 2 0 0 0 0 4h4" />
683
688
  </svg>
684
- {walletLabel}
689
+ <span className="truncate max-w-[120px]">{walletLabel}</span>
685
690
  </>
686
691
  ) : (
687
- widget.label
692
+ <span className="truncate max-w-[120px]">{widget.label}</span>
688
693
  )}
689
- </button>
694
+ </HeaderChip>
690
695
  )
691
696
  })}
692
- {visibleHeaderWidgets.length > 0 && (
693
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
694
- )}
695
697
  {modelName && (
696
698
  <div className="relative shrink-0" ref={modelSwitcherRef}>
697
- <Tip label="Switch LLM model">
699
+ <Tip label={`Switch model (${providerLabel})`}>
698
700
  <button
699
701
  type="button"
700
702
  onClick={() => {
@@ -702,16 +704,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
702
704
  setModelSwitcherOpen((o) => { if (!o) void loadProviders(); return !o })
703
705
  }}
704
706
  disabled={streaming}
705
- className="inline-flex items-center gap-1 text-[11px] text-text-3/45 font-mono shrink-0 cursor-pointer bg-transparent border-none px-1 py-0.5 rounded-[5px] hover:bg-white/[0.04] hover:text-text-3/70 transition-colors disabled:cursor-default disabled:hover:text-text-3/45"
707
+ className="inline-flex max-w-full items-center gap-1.5 rounded-[9px] border border-white/[0.06] bg-white/[0.03] px-2.5 py-1 text-[10px] font-600 text-text-3/70 backdrop-blur-sm transition-colors hover:border-white/[0.1] hover:bg-white/[0.06] hover:text-text-2 disabled:cursor-default disabled:opacity-60"
706
708
  >
707
- {modelName}
708
- <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-30">
709
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
710
+ <path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3Z" />
711
+ </svg>
712
+ <span className="truncate max-w-[min(42vw,220px)]">{mobile ? modelName : `${providerLabel} · ${modelName}`}</span>
713
+ <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-40">
709
714
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
710
715
  </svg>
711
716
  </button>
712
717
  </Tip>
713
718
  {modelSwitcherOpen && (
714
- <div className="absolute z-50 top-full left-0 mt-2 w-[280px] rounded-[12px] border border-white/[0.08] bg-surface backdrop-blur-md shadow-xl p-3">
719
+ <div className="absolute z-50 top-full right-0 sm:left-0 sm:right-auto mt-2 w-[min(320px,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] rounded-[12px] border border-white/[0.08] bg-surface backdrop-blur-md shadow-xl p-3">
715
720
  <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
716
721
  <div className="flex flex-wrap gap-1.5 mb-3">
717
722
  {providers.map((p) => (
@@ -733,119 +738,117 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
733
738
  onChange={(m) => void handleModelSwitch(session.provider, m)}
734
739
  models={currentModels}
735
740
  defaultModels={currentProviderInfo?.defaultModels}
741
+ credentialId={session.credentialId}
742
+ apiEndpoint={session.apiEndpoint}
743
+ supportsDiscovery={currentProviderInfo?.supportsModelDiscovery}
736
744
  className="px-2.5 py-1.5 rounded-[7px] text-[12px] font-mono bg-white/[0.04] hover:bg-white/[0.06] transition-colors"
737
745
  />
738
746
  </div>
739
747
  )}
740
748
  </div>
741
749
  )}
742
- <Tip label={`Open working directory: ${shortPath(session.cwd)}`}>
743
- <button
744
- type="button"
750
+ {threadContextLabel && (
751
+ <HeaderChip title={threadContextLabel}>
752
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
753
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" />
754
+ </svg>
755
+ <span className="truncate max-w-[min(42vw,220px)]">{threadContextLabel}</span>
756
+ </HeaderChip>
757
+ )}
758
+ <Tip label={`Open working directory: ${workspaceLabel}`}>
759
+ <HeaderChip
745
760
  onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
746
- className="inline-flex items-center shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-text-3/20 hover:text-text-3/50 hover:bg-white/[0.04] transition-colors"
761
+ title={workspaceLabel}
762
+ className="max-w-[min(44vw,220px)]"
747
763
  >
748
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
764
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
749
765
  <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
750
766
  </svg>
751
- </button>
767
+ <span className="truncate">{mobile ? 'Workspace' : workspaceLabel}</span>
768
+ </HeaderChip>
752
769
  </Tip>
753
- {/* Live agent status */}
754
- {(() => {
755
- const liveStatus = agentStatus || (missionState.status ? {
756
- goal: missionState.goal ?? undefined,
757
- status: missionState.status ?? undefined,
758
- summary: missionState.summary ?? undefined,
759
- nextAction: missionState.nextAction ?? undefined,
760
- } : null)
761
- if (!liveStatus) return null
762
- const statusColors: Record<string, string> = {
763
- idle: 'bg-text-3/40', progress: 'bg-blue-500', blocked: 'bg-amber-400', ok: 'bg-emerald-400',
764
- }
765
- const dotColor = statusColors[liveStatus.status || ''] || 'bg-text-3/40'
766
- return (
767
- <>
768
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
769
- {liveStatus.status && (
770
- <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] text-[9px] font-700 uppercase tracking-wider ${
771
- liveStatus.status === 'blocked' ? 'bg-amber-400/12 text-amber-300'
772
- : liveStatus.status === 'ok' ? 'bg-emerald-400/12 text-emerald-400'
773
- : liveStatus.status === 'progress' ? 'bg-blue-500/12 text-blue-400'
774
- : 'bg-white/[0.03] text-text-3/50'
775
- }`}>
776
- <span className={`w-1 h-1 rounded-full ${dotColor}`} />
777
- {liveStatus.status}
778
- </span>
779
- )}
780
- {liveStatus.goal && (
781
- <span className="text-[10px] text-text-3/40 font-mono truncate max-w-[180px]" title={liveStatus.goal}>
782
- {liveStatus.goal}
783
- </span>
784
- )}
785
- {liveStatus.nextAction && (
786
- <>
787
- <span className="text-[9px] text-text-3/20 shrink-0">→</span>
788
- <span className="text-[10px] text-text-3/35 font-mono truncate max-w-[140px]" title={liveStatus.nextAction}>
789
- {liveStatus.nextAction}
790
- </span>
791
- </>
792
- )}
793
- </>
794
- )
795
- })()}
770
+ {liveStatus?.status && (
771
+ <HeaderChip
772
+ className={`${
773
+ liveStatus.status === 'blocked' ? 'bg-amber-400/12 border-amber-400/15 text-amber-300'
774
+ : liveStatus.status === 'ok' ? 'bg-emerald-400/12 border-emerald-400/15 text-emerald-400'
775
+ : liveStatus.status === 'progress' ? 'bg-blue-500/12 border-blue-500/15 text-blue-400'
776
+ : 'text-text-3/60'
777
+ }`}
778
+ title={liveStatus.goal || liveStatus.summary || liveStatus.nextAction || liveStatus.status}
779
+ >
780
+ <span className={`w-1.5 h-1.5 rounded-full ${
781
+ liveStatus.status === 'blocked' ? 'bg-amber-300'
782
+ : liveStatus.status === 'ok' ? 'bg-emerald-400'
783
+ : liveStatus.status === 'progress' ? 'bg-blue-400'
784
+ : 'bg-text-3/30'
785
+ }`} />
786
+ {liveStatus.status}
787
+ </HeaderChip>
788
+ )}
789
+ {!mobile && liveStatus?.nextAction && (
790
+ <span className="text-[10px] text-text-3/45 font-mono truncate max-w-[min(34vw,220px)]" title={liveStatus.nextAction}>
791
+ Next: {liveStatus.nextAction}
792
+ </span>
793
+ )}
796
794
  </div>
797
795
  </div>
798
796
 
799
- {/* Heartbeat compound control */}
800
- {heartbeatSupported && (
801
- <div className="flex items-center rounded-[8px] shrink-0" style={{ background: 'rgba(255,255,255,0.025)' }}>
802
- <Tip label={heartbeatWillRun ? 'Disable heartbeat periodic check-ins' : 'Enable heartbeat — periodic check-ins'}>
803
- <button
804
- onClick={handleToggleHeartbeat}
805
- disabled={heartbeatSaving}
806
- className={`flex items-center gap-1.5 pl-2.5 pr-1.5 py-1 transition-colors cursor-pointer border-none text-[11px] font-600
807
- ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/60 hover:bg-white/[0.04]'}`}
808
- >
809
- <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/30'}`} />
810
- HB
811
- {heartbeatEnabled && !loopIsOngoing && !heartbeatExplicitOptIn && (
812
- <span className="text-[9px] text-text-3/40">(bounded)</span>
813
- )}
814
- </button>
815
- </Tip>
816
- <div className="relative" ref={hbDropdownRef}>
817
- <Tip label="Set heartbeat interval">
797
+ <div className={`flex items-center gap-2 shrink-0 ${mobile ? 'w-full justify-between pt-1' : 'ml-auto'}`}>
798
+ {/* Heartbeat compound control */}
799
+ {heartbeatSupported && (
800
+ <div className="flex items-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] shrink-0">
801
+ <Tip label={heartbeatWillRun ? 'Disable heartbeat — periodic check-ins' : 'Enable heartbeat — periodic check-ins'}>
818
802
  <button
819
- onClick={() => setHbDropdownOpen((o) => !o)}
803
+ onClick={handleToggleHeartbeat}
820
804
  disabled={heartbeatSaving}
821
- className="flex items-center gap-0.5 pl-1 pr-2 py-1 text-text-3/50 hover:text-text-3/70 hover:bg-white/[0.04] transition-colors cursor-pointer border-none"
805
+ aria-pressed={heartbeatWillRun}
806
+ className={`flex items-center gap-1.5 pl-2.5 pr-2 py-1.5 rounded-l-[11px] transition-colors cursor-pointer border-none text-[11px] font-600
807
+ ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/70 hover:bg-white/[0.04]'}`}
822
808
  >
823
- <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
824
- <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
825
- <polyline points="6 9 12 15 18 9" />
826
- </svg>
809
+ <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : heartbeatEnabled ? 'bg-amber-300' : 'bg-text-3/30'}`} />
810
+ <span className="hidden sm:inline">Heartbeat</span>
811
+ <span className="sm:hidden">HB</span>
812
+ <span className={`hidden md:inline text-[9px] uppercase tracking-wider ${
813
+ heartbeatWillRun ? 'text-emerald-300/80' : heartbeatEnabled ? 'text-amber-300/70' : 'text-text-3/40'
814
+ }`}>
815
+ {heartbeatWillRun ? 'On' : heartbeatEnabled ? 'Bounded' : 'Off'}
816
+ </span>
827
817
  </button>
828
818
  </Tip>
829
- {hbDropdownOpen && (
830
- <div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
831
- {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
832
- <button
833
- key={sec}
834
- onClick={() => handleSelectHeartbeatInterval(sec)}
835
- className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
836
- ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
837
- >
838
- {formatDuration(sec)}
839
- </button>
840
- ))}
841
- </div>
842
- )}
819
+ <div className="relative" ref={hbDropdownRef}>
820
+ <Tip label="Set heartbeat interval">
821
+ <button
822
+ onClick={() => setHbDropdownOpen((o) => !o)}
823
+ disabled={heartbeatSaving}
824
+ className="flex items-center gap-0.5 pl-1 pr-2.5 py-1.5 text-text-3/60 hover:text-text-2 hover:bg-white/[0.04] transition-colors cursor-pointer border-none rounded-r-[11px]"
825
+ >
826
+ <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
827
+ <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
828
+ <polyline points="6 9 12 15 18 9" />
829
+ </svg>
830
+ </button>
831
+ </Tip>
832
+ {hbDropdownOpen && (
833
+ <div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[88px]">
834
+ {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
835
+ <button
836
+ key={sec}
837
+ onClick={() => handleSelectHeartbeatInterval(sec)}
838
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
839
+ ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
840
+ >
841
+ {formatDuration(sec)}
842
+ </button>
843
+ ))}
844
+ </div>
845
+ )}
846
+ </div>
843
847
  </div>
844
- </div>
845
- )}
848
+ )}
846
849
 
847
- {/* Action buttons */}
848
- <div className="flex items-center shrink-0">
850
+ {/* Action buttons */}
851
+ <div className="flex items-center shrink-0 rounded-[12px] border border-white/[0.06] bg-white/[0.03] shadow-[inset_0_1px_0_rgba(255,255,255,0.04)] p-1">
849
852
  {streaming && (
850
853
  <>
851
854
  <IconButton onClick={onStop} variant="danger" tooltip="Stop" aria-label="Stop generation" size="sm">
@@ -885,18 +888,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
885
888
  </IconButton>
886
889
  )}
887
890
  <div className="w-px h-3.5 bg-white/[0.06] mx-0.5" />
888
- <IconButton onClick={() => setDebugOpen(!debugOpen)} active={debugOpen} tooltip="Debug" aria-label="Toggle debug panel" size="sm">
889
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
890
- <path d="M12 20V10" /><path d="M18 20V4" /><path d="M6 20v-4" />
891
+ <IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} tooltip="More" aria-label="Chat menu" size="sm">
892
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
893
+ <circle cx="12" cy="6" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="12" cy="18" r="1" />
891
894
  </svg>
892
895
  </IconButton>
893
- {(!agent || mobile) && (
894
- <IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} tooltip="Menu" aria-label="Chat menu" size="sm">
895
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
896
- <circle cx="12" cy="6" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="12" cy="18" r="1" />
897
- </svg>
898
- </IconButton>
899
- )}
900
896
  {agent && (
901
897
  <IconButton onClick={() => setInspectorOpen(!inspectorOpen)} active={inspectorOpen} tooltip="Settings" aria-label="Toggle inspector" size="sm">
902
898
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -906,76 +902,18 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
906
902
  </IconButton>
907
903
  )}
908
904
  </div>
905
+ </div>
909
906
  </div>
910
907
 
911
- {/* Context bar: tools, mission controls, links */}
908
+ {/* Context bar: tools and links */}
912
909
  {hasContextBar && (
913
- <div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
914
- {hasMainLoop && (
915
- <>
916
- <Tip label={missionPaused ? 'Resume mission loop' : 'Pause mission loop'}>
917
- <button
918
- onClick={handleToggleMissionPause}
919
- disabled={mainLoopSaving}
920
- className={`flex items-center gap-1.5 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
921
- ${missionPaused ? 'bg-amber-500/10 hover:bg-amber-500/18 text-amber-300' : 'bg-emerald-500/8 hover:bg-emerald-500/12 text-emerald-400'}`}
922
- >
923
- <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
924
- {missionPaused ? 'Paused' : 'Live'}
925
- </button>
926
- </Tip>
927
-
928
- <Tip label={missionMode === 'autonomous' ? 'Switch to assisted mode' : 'Switch to autonomous mode'}>
929
- <button
930
- onClick={handleToggleMissionMode}
931
- disabled={mainLoopSaving}
932
- className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
933
- ${missionMode === 'autonomous' ? 'bg-indigo-500/12 hover:bg-indigo-500/20 text-indigo-300' : 'bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60'}`}
934
- >
935
- {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
936
- </button>
937
- </Tip>
938
- <Tip label="Run one iteration of the mission loop">
939
- <button
940
- onClick={handleNudgeMission}
941
- disabled={mainLoopSaving || missionPaused}
942
- className="px-2 py-1 rounded-[7px] bg-blue-500/8 hover:bg-blue-500/15 text-blue-400 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600"
943
- >
944
- Nudge
945
- </button>
946
- </Tip>
947
- <Tip label="Set or edit the mission goal">
948
- <button
949
- onClick={handleOpenGoalModal}
950
- disabled={mainLoopSaving}
951
- className="px-2 py-1 rounded-[7px] bg-fuchsia-500/8 hover:bg-fuchsia-500/15 text-fuchsia-300 transition-colors cursor-pointer border-none text-[10px] font-600"
952
- >
953
- Goal
954
- </button>
955
- </Tip>
956
- {missionEventsCount > 0 && (
957
- <Tip label="Clear queued mission events">
958
- <button
959
- onClick={handleClearMissionEvents}
960
- disabled={mainLoopSaving}
961
- className="px-2 py-1 rounded-[7px] bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60 transition-colors cursor-pointer border-none text-[10px] font-600"
962
- >
963
- Events {missionEventsCount}
964
- </button>
965
- </Tip>
966
- )}
967
- <span className="text-[9px] text-text-3/40 uppercase tracking-wider shrink-0">
968
- {missionStatus}{missionMomentum !== null ? ` · ${missionMomentum}` : ''}
969
- </span>
970
- {mainLoopError && <span className="text-[9px] text-red-300/80 truncate max-w-[240px]" title={mainLoopError}>{mainLoopError}</span>}
971
- {mainLoopNotice && <span className="text-[9px] text-emerald-300/80 truncate max-w-[200px]" title={mainLoopNotice}>{mainLoopNotice}</span>}
972
- </>
973
- )}
910
+ <div className="border-t border-white/[0.05] bg-black/[0.08] px-4 py-2">
911
+ <div className="flex items-center gap-1.5 flex-wrap">
974
912
  {hasMemoryLink && (
975
913
  <Tip label="View agent memories">
976
914
  <button
977
915
  onClick={() => { setMemoryAgentFilter(session.agentId!); setActiveView('memory'); setSidebarOpen(true) }}
978
- className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-accent-soft/40 hover:bg-accent-soft/70 transition-colors cursor-pointer text-[10px] font-600 text-accent-bright/55 hover:text-accent-bright/80 shrink-0"
916
+ className="flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-accent-soft/40 hover:bg-accent-soft/70 transition-colors cursor-pointer text-[10px] font-600 text-accent-bright/55 hover:text-accent-bright/80 shrink-0"
979
917
  >
980
918
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
981
919
  <ellipse cx="12" cy="5" rx="9" ry="3" /><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" /><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
@@ -1007,7 +945,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1007
945
  </button>
1008
946
  </Tip>
1009
947
  {sourceDropdownOpen && (
1010
- <div className="absolute top-full left-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[140px]">
948
+ <div className="absolute top-full right-0 sm:left-0 sm:right-auto mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[160px] max-w-[calc(100vw-2rem)]">
1011
949
  <button
1012
950
  onClick={() => { onConnectorFilterChange(null); setSourceDropdownOpen(false) }}
1013
951
  className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none flex items-center gap-2 ${
@@ -1072,12 +1010,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1072
1010
  <Tip label="Copy CLI resume command">
1073
1011
  <button
1074
1012
  onClick={handleCopySessionId}
1075
- className="flex items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
1013
+ className="flex min-w-0 items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
1076
1014
  >
1077
1015
  <svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/40 shrink-0">
1078
1016
  <path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
1079
1017
  </svg>
1080
- <span className="text-[10px] font-mono text-text-3/40 group-hover/resume:text-text-3/60 truncate max-w-[180px]">
1018
+ <span className="text-[10px] font-mono text-text-3/40 group-hover/resume:text-text-3/60 truncate max-w-[min(46vw,220px)]">
1081
1019
  {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
1082
1020
  </span>
1083
1021
  </button>
@@ -1085,7 +1023,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1085
1023
  <Tip label="Dismiss resume handle">
1086
1024
  <button
1087
1025
  onClick={handleDismissResumeHandle}
1088
- className="px-1 py-1 rounded-r-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer opacity-0 group-hover/resume:opacity-100"
1026
+ className="px-1 py-1 rounded-r-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer opacity-60 md:opacity-0 md:group-hover/resume:opacity-100 group-focus-within/resume:opacity-100"
1089
1027
  >
1090
1028
  <svg width="8" height="8" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40 hover:text-text-3">
1091
1029
  <path d="M4 4l8 8M12 4l-8 8" />
@@ -1111,51 +1049,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1111
1049
  </Tip>
1112
1050
  )}
1113
1051
  </div>
1052
+ </div>
1114
1053
  )}
1115
1054
 
1116
1055
  </header>
1117
-
1118
- {/* Goal modal — fixed to viewport, not constrained by header */}
1119
- {goalModalOpen && (
1120
- <div className="fixed inset-0 z-[200] flex items-center justify-center" onClick={() => setGoalModalOpen(false)}>
1121
- <div className="absolute inset-0 bg-black/50 backdrop-blur-sm" />
1122
- <div
1123
- className="relative w-[420px] max-w-[90vw] rounded-[16px] border border-white/[0.08] bg-surface shadow-2xl p-6"
1124
- onClick={(e) => e.stopPropagation()}
1125
- >
1126
- <h4 className="font-display text-[15px] font-700 text-text mb-1">Set Mission Goal</h4>
1127
- <p className="text-[11px] text-text-3/60 mb-4">Define what this agent should work towards.</p>
1128
- <textarea
1129
- ref={goalInputRef}
1130
- value={goalDraft}
1131
- onChange={(e) => setGoalDraft(e.target.value)}
1132
- onKeyDown={(e) => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) handleSubmitGoal(); if (e.key === 'Escape') setGoalModalOpen(false) }}
1133
- placeholder="Describe the agent's mission..."
1134
- rows={4}
1135
- className="w-full py-2.5 px-3 rounded-[10px] text-[13px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30 mb-1 resize-y min-h-[80px]"
1136
- style={{ fontFamily: 'inherit' }}
1137
- />
1138
- <p className="text-[10px] text-text-3/40 mb-3">Cmd+Enter to submit</p>
1139
- <div className="flex items-center justify-end gap-2">
1140
- <button
1141
- onClick={() => setGoalModalOpen(false)}
1142
- className="px-4 py-2 rounded-[8px] text-[12px] font-600 text-text-3 bg-white/[0.04] hover:bg-white/[0.08] transition-colors cursor-pointer border-none"
1143
- style={{ fontFamily: 'inherit' }}
1144
- >
1145
- Cancel
1146
- </button>
1147
- <button
1148
- onClick={handleSubmitGoal}
1149
- disabled={!goalDraft.trim()}
1150
- className="px-4 py-2 rounded-[8px] text-[12px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-soft/80 transition-colors cursor-pointer border border-accent-bright/20 disabled:opacity-40 disabled:cursor-default"
1151
- style={{ fontFamily: 'inherit' }}
1152
- >
1153
- Set Goal
1154
- </button>
1155
- </div>
1156
- </div>
1157
- </div>
1158
- )}
1159
1056
  </>
1160
1057
  )
1161
1058
  }