@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -18,6 +18,7 @@ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip
18
18
  import { toast } from 'sonner'
19
19
  import type { ProviderType } from '@/types'
20
20
  import { copyTextToClipboard } from '@/lib/clipboard'
21
+ import { buildOpenClawMainSessionKey } from '@/lib/openclaw-agent-id'
21
22
  import { useWs } from '@/hooks/use-ws'
22
23
 
23
24
  function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
@@ -32,6 +33,40 @@ function Tip({ label, children, side = 'bottom' }: { label: string; children: Re
32
33
  )
33
34
  }
34
35
 
36
+ function HeaderChip({
37
+ children,
38
+ title,
39
+ onClick,
40
+ className = '',
41
+ active = false,
42
+ }: {
43
+ children: ReactNode
44
+ title?: string
45
+ onClick?: () => void
46
+ className?: string
47
+ active?: boolean
48
+ }) {
49
+ 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 ${
50
+ active
51
+ ? 'border-accent-bright/20 bg-accent-soft/50 text-accent-bright'
52
+ : 'border-white/[0.06] bg-white/[0.03] text-text-3/68'
53
+ } ${onClick ? 'cursor-pointer hover:border-white/[0.1] hover:bg-white/[0.06] hover:text-text-2' : ''} ${className}`
54
+
55
+ if (onClick) {
56
+ return (
57
+ <button type="button" onClick={onClick} title={title} className={baseClass}>
58
+ {children}
59
+ </button>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <span title={title} className={baseClass}>
65
+ {children}
66
+ </span>
67
+ )
68
+ }
69
+
35
70
  function shortPath(p: string): string {
36
71
  return (p || '').replace(/^\/Users\/\w+/, '~')
37
72
  }
@@ -100,6 +135,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
100
135
  const providers = useAppStore((s) => s.providers)
101
136
  const loadProviders = useAppStore((s) => s.loadProviders)
102
137
  const modelName = session.model || agent?.model || ''
138
+ const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
103
139
  const [modelSwitcherOpen, setModelSwitcherOpen] = useState(false)
104
140
  const modelSwitcherRef = useRef<HTMLDivElement>(null)
105
141
  const [copied, setCopied] = useState(false)
@@ -108,9 +144,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
108
144
  const hbDropdownRef = useRef<HTMLDivElement>(null)
109
145
  const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
110
146
  const sourceDropdownRef = useRef<HTMLDivElement>(null)
111
- const [mainLoopSaving, setMainLoopSaving] = useState(false)
112
- const [mainLoopError, setMainLoopError] = useState('')
113
- const [mainLoopNotice, setMainLoopNotice] = useState('')
114
147
  const [syncingHistory, setSyncingHistory] = useState(false)
115
148
  const [syncResult, setSyncResult] = useState('')
116
149
  const [renaming, setRenaming] = useState(false)
@@ -122,9 +155,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
122
155
  const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
123
156
  const [walletBalance, setWalletBalance] = useState<number | null>(null)
124
157
  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
158
 
129
159
  useEffect(() => {
130
160
  api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
@@ -150,6 +180,46 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
150
180
  }, [fetchWalletBalance])
151
181
  useWs('wallets', fetchWalletBalance)
152
182
 
183
+ const workspaceLabel = useMemo(() => shortPath(session.cwd), [session.cwd])
184
+ const liveStatus = agentStatus || null
185
+ const threadContextLabel = useMemo(() => {
186
+ const title = session.connectorContext?.threadTitle?.trim()
187
+ if (title) return title
188
+ const persona = session.connectorContext?.threadPersonaLabel?.trim()
189
+ if (persona) return persona
190
+ return null
191
+ }, [session.connectorContext?.threadPersonaLabel, session.connectorContext?.threadTitle])
192
+ const connectorPresenceMeta = useMemo(() => {
193
+ if (!connector) return null
194
+ const lastAt = connectorPresence?.lastMessageAt
195
+ if (!lastAt) {
196
+ return {
197
+ label: 'Idle',
198
+ dotClass: 'bg-text-3/30',
199
+ textClass: 'text-text-3/45',
200
+ }
201
+ }
202
+ const ago = Date.now() - lastAt
203
+ if (ago < 5 * 60_000) {
204
+ return {
205
+ label: 'Active',
206
+ dotClass: 'bg-emerald-400',
207
+ textClass: 'text-emerald-400',
208
+ }
209
+ }
210
+ if (ago < 30 * 60_000) {
211
+ return {
212
+ label: `${Math.floor(ago / 60_000)}m ago`,
213
+ dotClass: 'bg-amber-400',
214
+ textClass: 'text-amber-300',
215
+ }
216
+ }
217
+ return {
218
+ label: 'Idle',
219
+ dotClass: 'bg-text-3/30',
220
+ textClass: 'text-text-3/45',
221
+ }
222
+ }, [connector, connectorPresence?.lastMessageAt])
153
223
 
154
224
  const visibleHeaderWidgets = useMemo(() => {
155
225
  const seen = new Set<string>()
@@ -161,6 +231,25 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
161
231
  })
162
232
  }, [headerWidgets])
163
233
 
234
+ const walletHeaderMeta = useMemo(() => {
235
+ if (!agent?.id) {
236
+ return {
237
+ label: 'Wallets',
238
+ title: 'Open wallets',
239
+ }
240
+ }
241
+ if (!agent.walletId) {
242
+ return {
243
+ label: 'Create wallet',
244
+ title: 'Create wallet',
245
+ }
246
+ }
247
+ return {
248
+ label: walletBalance !== null ? `${walletBalance.toFixed(3)} SOL` : 'Wallet',
249
+ title: 'View wallet',
250
+ }
251
+ }, [agent?.id, agent?.walletId, walletBalance])
252
+
164
253
  const handleHeaderWidgetClick = (widgetId: string) => {
165
254
  if (widgetId === 'wallet-status') {
166
255
  if (agent?.id) setWalletPanelAgentId(agent.id)
@@ -270,13 +359,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
270
359
  }
271
360
  }, [appSettings, agent, session])
272
361
  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
362
 
281
363
  const handleToggleHeartbeat = async () => {
282
364
  if (!heartbeatSupported || heartbeatSaving) return
@@ -322,68 +404,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
322
404
  }
323
405
  }
324
406
 
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
407
  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
408
+ const openclawSessionKey = isOpenClawAgent ? buildOpenClawMainSessionKey(agent?.name) : null
387
409
 
388
410
  const handleSyncHistory = async () => {
389
411
  if (!openclawSessionKey || syncingHistory) return
@@ -511,36 +533,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
511
533
  }, [session.name, loadConnectors])
512
534
 
513
535
  useEffect(() => {
514
- setMainLoopError('')
515
- setMainLoopNotice('')
516
536
  setModelSwitcherOpen(false)
517
537
  }, [session.id])
518
538
 
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
539
+ // Context bar shows for tools, memories, source filter, task links, resume handles, browser
526
540
  const hasToolToggles = ((agent?.plugins?.length ?? 0) > 0) || ((session.plugins?.length ?? 0) > 0)
527
541
  const hasMemoryLink = !!(agent && session.plugins?.includes('memory'))
528
542
  const hasSourceFilter = !!hasMultipleSources
529
- const hasContextBar = !!(hasMainLoop || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
543
+ const hasContextBar = !!(hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
530
544
 
531
545
  return (
532
546
  <>
533
547
  <header
534
548
  className="relative z-20 border-b border-white/[0.06] shrink-0"
535
549
  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%)',
550
+ 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
551
  backdropFilter: 'blur(20px) saturate(1.4)',
538
552
  WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
539
553
  ...(mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : {}),
540
554
  }}
541
555
  >
542
556
  {/* Main row */}
543
- <div className="flex items-center gap-2 px-3.5 py-1.5 min-h-[48px]">
557
+ <div className="flex flex-wrap items-start gap-3 px-4 py-2.5 min-h-[64px]">
544
558
  {/* Back button */}
545
559
  {onBack && (
546
560
  <IconButton onClick={onBack} aria-label="Go back" size="sm">
@@ -581,10 +595,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
581
595
  )}
582
596
 
583
597
  {/* 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">
598
+ <div className="min-w-0 flex-1">
599
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
588
600
  {renaming && agent ? (
589
601
  <span ref={renameContainerRef} className="inline-flex items-center gap-2">
590
602
  <input
@@ -602,99 +614,90 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
602
614
  {renameSaving && <span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-accent-bright animate-spin shrink-0" />}
603
615
  {renameError && <span className="text-[10px] text-red-400 shrink-0">{renameError}</span>}
604
616
  </span>
617
+ ) : agent ? (
618
+ <button
619
+ type="button"
620
+ onClick={startRename}
621
+ title="Rename agent"
622
+ 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"
623
+ >
624
+ <span className="font-display text-[16px] font-700 truncate tracking-[-0.02em] text-text transition-colors group-hover/title:text-accent-bright">
625
+ {(session.shortcutForAgentId && agent.id === session.shortcutForAgentId) || agent.threadSessionId === session.id
626
+ ? agent.name
627
+ : session.name}
628
+ </span>
629
+ <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">
630
+ <path d="M12 20h9" />
631
+ <path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z" />
632
+ </svg>
633
+ </button>
605
634
  ) : (
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>
635
+ <span className="font-display text-[16px] font-700 truncate tracking-[-0.02em] text-text">{session.name}</span>
614
636
  )}
615
637
  {connector && connectorMeta && (
616
638
  <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"
639
+ 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
640
  style={{
619
641
  color: connectorMeta.color,
620
- backgroundColor: `${connectorMeta.color}10`,
621
- borderColor: `${connectorMeta.color}20`,
642
+ backgroundColor: `${connectorMeta.color}12`,
643
+ borderColor: `${connectorMeta.color}22`,
622
644
  }}
623
645
  title={`${connector.name} connector`}
624
646
  >
625
647
  <ConnectorPlatformIcon platform={connector.platform} size={10} />
626
- {connectorMeta.label}
648
+ <span className="truncate max-w-[140px]">{connectorMeta.label}</span>
627
649
  </span>
628
650
  )}
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>
651
+ {connectorPresenceMeta && (
652
+ <HeaderChip className={`${connectorPresenceMeta.textClass} shrink-0`}>
653
+ <span className={`w-1.5 h-1.5 rounded-full ${connectorPresenceMeta.dotClass}`} />
654
+ {connectorPresenceMeta.label}
655
+ </HeaderChip>
656
+ )}
657
+ {agent?.platformAssignScope === 'all' && (
658
+ <HeaderChip className="bg-amber-500/10 border-amber-500/15 text-amber-400 shrink-0">Delegates</HeaderChip>
652
659
  )}
653
660
  {streaming && (
654
- <span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
661
+ <HeaderChip className="bg-accent-soft/60 border-accent-bright/20 text-accent-bright shrink-0">
662
+ <span className="w-1.5 h-1.5 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
663
+ Responding
664
+ </HeaderChip>
655
665
  )}
656
666
  </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>
667
+ <div className="mt-1.5 flex min-w-0 flex-wrap items-center gap-1.5">
668
+ {hasToolToggles && <ChatToolToggles session={session} />}
663
669
  {visibleHeaderWidgets.map((widget) => {
664
670
  const actionable = widget.id === 'wallet-status'
665
- const walletLabel = walletBalance !== null
666
- ? `${walletBalance.toFixed(3)} SOL`
671
+ const walletLabel = actionable
672
+ ? walletHeaderMeta.label
667
673
  : (widget.label || 'Wallet')
674
+ const widgetTitle = actionable
675
+ ? walletHeaderMeta.title
676
+ : widget.label
668
677
  return (
669
- <button
678
+ <HeaderChip
670
679
  key={widget.id}
671
- type="button"
672
680
  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}
681
+ title={widgetTitle}
682
+ className={actionable ? 'text-text-3/80' : ''}
677
683
  >
678
684
  {actionable ? (
679
685
  <>
680
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
686
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
681
687
  <rect x="2" y="6" width="20" height="14" rx="2" />
682
688
  <path d="M22 10H18a2 2 0 0 0 0 4h4" />
683
689
  </svg>
684
- {walletLabel}
690
+ <span className="truncate max-w-[120px]">{walletLabel}</span>
685
691
  </>
686
692
  ) : (
687
- widget.label
693
+ <span className="truncate max-w-[120px]">{widget.label}</span>
688
694
  )}
689
- </button>
695
+ </HeaderChip>
690
696
  )
691
697
  })}
692
- {visibleHeaderWidgets.length > 0 && (
693
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
694
- )}
695
698
  {modelName && (
696
699
  <div className="relative shrink-0" ref={modelSwitcherRef}>
697
- <Tip label="Switch LLM model">
700
+ <Tip label={`Switch model (${providerLabel})`}>
698
701
  <button
699
702
  type="button"
700
703
  onClick={() => {
@@ -702,16 +705,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
702
705
  setModelSwitcherOpen((o) => { if (!o) void loadProviders(); return !o })
703
706
  }}
704
707
  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"
708
+ 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
709
  >
707
- {modelName}
708
- <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-30">
710
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
711
+ <path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3Z" />
712
+ </svg>
713
+ <span className="truncate max-w-[min(42vw,220px)]">{mobile ? modelName : `${providerLabel} · ${modelName}`}</span>
714
+ <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-40">
709
715
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
710
716
  </svg>
711
717
  </button>
712
718
  </Tip>
713
719
  {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">
720
+ <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
721
  <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
716
722
  <div className="flex flex-wrap gap-1.5 mb-3">
717
723
  {providers.map((p) => (
@@ -739,113 +745,108 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
739
745
  )}
740
746
  </div>
741
747
  )}
742
- <Tip label={`Open working directory: ${shortPath(session.cwd)}`}>
743
- <button
744
- type="button"
748
+ {threadContextLabel && (
749
+ <HeaderChip title={threadContextLabel}>
750
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
751
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" />
752
+ </svg>
753
+ <span className="truncate max-w-[min(42vw,220px)]">{threadContextLabel}</span>
754
+ </HeaderChip>
755
+ )}
756
+ <Tip label={`Open working directory: ${workspaceLabel}`}>
757
+ <HeaderChip
745
758
  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"
759
+ title={workspaceLabel}
760
+ className="max-w-[min(44vw,220px)]"
747
761
  >
748
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
762
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="shrink-0">
749
763
  <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
764
  </svg>
751
- </button>
765
+ <span className="truncate">{mobile ? 'Workspace' : workspaceLabel}</span>
766
+ </HeaderChip>
752
767
  </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
- })()}
768
+ {liveStatus?.status && (
769
+ <HeaderChip
770
+ className={`${
771
+ liveStatus.status === 'blocked' ? 'bg-amber-400/12 border-amber-400/15 text-amber-300'
772
+ : liveStatus.status === 'ok' ? 'bg-emerald-400/12 border-emerald-400/15 text-emerald-400'
773
+ : liveStatus.status === 'progress' ? 'bg-blue-500/12 border-blue-500/15 text-blue-400'
774
+ : 'text-text-3/60'
775
+ }`}
776
+ title={liveStatus.goal || liveStatus.summary || liveStatus.nextAction || liveStatus.status}
777
+ >
778
+ <span className={`w-1.5 h-1.5 rounded-full ${
779
+ liveStatus.status === 'blocked' ? 'bg-amber-300'
780
+ : liveStatus.status === 'ok' ? 'bg-emerald-400'
781
+ : liveStatus.status === 'progress' ? 'bg-blue-400'
782
+ : 'bg-text-3/30'
783
+ }`} />
784
+ {liveStatus.status}
785
+ </HeaderChip>
786
+ )}
787
+ {!mobile && liveStatus?.nextAction && (
788
+ <span className="text-[10px] text-text-3/45 font-mono truncate max-w-[min(34vw,220px)]" title={liveStatus.nextAction}>
789
+ Next: {liveStatus.nextAction}
790
+ </span>
791
+ )}
796
792
  </div>
797
793
  </div>
798
794
 
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">
795
+ <div className={`flex items-center gap-2 shrink-0 ${mobile ? 'w-full justify-between pt-1' : 'ml-auto'}`}>
796
+ {/* Heartbeat compound control */}
797
+ {heartbeatSupported && (
798
+ <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">
799
+ <Tip label={heartbeatWillRun ? 'Disable heartbeat — periodic check-ins' : 'Enable heartbeat — periodic check-ins'}>
818
800
  <button
819
- onClick={() => setHbDropdownOpen((o) => !o)}
801
+ onClick={handleToggleHeartbeat}
820
802
  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"
803
+ aria-pressed={heartbeatWillRun}
804
+ 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
805
+ ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/70 hover:bg-white/[0.04]'}`}
822
806
  >
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>
807
+ <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : heartbeatEnabled ? 'bg-amber-300' : 'bg-text-3/30'}`} />
808
+ <span className="hidden sm:inline">Heartbeat</span>
809
+ <span className="sm:hidden">HB</span>
810
+ <span className={`hidden md:inline text-[9px] uppercase tracking-wider ${
811
+ heartbeatWillRun ? 'text-emerald-300/80' : heartbeatEnabled ? 'text-amber-300/70' : 'text-text-3/40'
812
+ }`}>
813
+ {heartbeatWillRun ? 'On' : heartbeatEnabled ? 'Bounded' : 'Off'}
814
+ </span>
827
815
  </button>
828
816
  </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
- )}
817
+ <div className="relative" ref={hbDropdownRef}>
818
+ <Tip label="Set heartbeat interval">
819
+ <button
820
+ onClick={() => setHbDropdownOpen((o) => !o)}
821
+ disabled={heartbeatSaving}
822
+ 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]"
823
+ >
824
+ <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
825
+ <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
826
+ <polyline points="6 9 12 15 18 9" />
827
+ </svg>
828
+ </button>
829
+ </Tip>
830
+ {hbDropdownOpen && (
831
+ <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]">
832
+ {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
833
+ <button
834
+ key={sec}
835
+ onClick={() => handleSelectHeartbeatInterval(sec)}
836
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
837
+ ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
838
+ >
839
+ {formatDuration(sec)}
840
+ </button>
841
+ ))}
842
+ </div>
843
+ )}
844
+ </div>
843
845
  </div>
844
- </div>
845
- )}
846
+ )}
846
847
 
847
- {/* Action buttons */}
848
- <div className="flex items-center shrink-0">
848
+ {/* Action buttons */}
849
+ <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
850
  {streaming && (
850
851
  <>
851
852
  <IconButton onClick={onStop} variant="danger" tooltip="Stop" aria-label="Stop generation" size="sm">
@@ -906,76 +907,18 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
906
907
  </IconButton>
907
908
  )}
908
909
  </div>
910
+ </div>
909
911
  </div>
910
912
 
911
- {/* Context bar: tools, mission controls, links */}
913
+ {/* Context bar: tools and links */}
912
914
  {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
- )}
915
+ <div className="border-t border-white/[0.05] bg-black/[0.08] px-4 py-2">
916
+ <div className="flex items-center gap-1.5 flex-wrap">
974
917
  {hasMemoryLink && (
975
918
  <Tip label="View agent memories">
976
919
  <button
977
920
  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"
921
+ 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
922
  >
980
923
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
981
924
  <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 +950,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1007
950
  </button>
1008
951
  </Tip>
1009
952
  {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]">
953
+ <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
954
  <button
1012
955
  onClick={() => { onConnectorFilterChange(null); setSourceDropdownOpen(false) }}
1013
956
  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 +1015,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1072
1015
  <Tip label="Copy CLI resume command">
1073
1016
  <button
1074
1017
  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"
1018
+ 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
1019
  >
1077
1020
  <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
1021
  <path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
1079
1022
  </svg>
1080
- <span className="text-[10px] font-mono text-text-3/40 group-hover/resume:text-text-3/60 truncate max-w-[180px]">
1023
+ <span className="text-[10px] font-mono text-text-3/40 group-hover/resume:text-text-3/60 truncate max-w-[min(46vw,220px)]">
1081
1024
  {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
1082
1025
  </span>
1083
1026
  </button>
@@ -1085,7 +1028,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1085
1028
  <Tip label="Dismiss resume handle">
1086
1029
  <button
1087
1030
  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"
1031
+ 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
1032
  >
1090
1033
  <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
1034
  <path d="M4 4l8 8M12 4l-8 8" />
@@ -1111,51 +1054,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1111
1054
  </Tip>
1112
1055
  )}
1113
1056
  </div>
1057
+ </div>
1114
1058
  )}
1115
1059
 
1116
1060
  </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
1061
  </>
1160
1062
  )
1161
1063
  }