@swarmclawai/swarmclaw 0.7.1 → 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 (237) hide show
  1. package/README.md +155 -150
  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 +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  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/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
3
+ import { useEffect, useState, useMemo, useRef, useCallback, type ReactNode } from 'react'
4
4
  import type { Session } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
@@ -14,11 +14,59 @@ import {
14
14
  } from '@/components/shared/connector-platform-icon'
15
15
  import { AgentAvatar } from '@/components/agents/agent-avatar'
16
16
  import { ModelCombobox } from '@/components/shared/model-combobox'
17
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
17
18
  import { toast } from 'sonner'
18
19
  import type { ProviderType } from '@/types'
19
20
  import { copyTextToClipboard } from '@/lib/clipboard'
21
+ import { buildOpenClawMainSessionKey } from '@/lib/openclaw-agent-id'
20
22
  import { useWs } from '@/hooks/use-ws'
21
23
 
24
+ function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
25
+ return (
26
+ <Tooltip>
27
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
28
+ <TooltipContent side={side} sideOffset={6}
29
+ className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[8px] px-2.5 py-1.5 text-[11px] z-[100]">
30
+ {label}
31
+ </TooltipContent>
32
+ </Tooltip>
33
+ )
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
+
22
70
  function shortPath(p: string): string {
23
71
  return (p || '').replace(/^\/Users\/\w+/, '~')
24
72
  }
@@ -87,6 +135,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
87
135
  const providers = useAppStore((s) => s.providers)
88
136
  const loadProviders = useAppStore((s) => s.loadProviders)
89
137
  const modelName = session.model || agent?.model || ''
138
+ const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
90
139
  const [modelSwitcherOpen, setModelSwitcherOpen] = useState(false)
91
140
  const modelSwitcherRef = useRef<HTMLDivElement>(null)
92
141
  const [copied, setCopied] = useState(false)
@@ -95,9 +144,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
95
144
  const hbDropdownRef = useRef<HTMLDivElement>(null)
96
145
  const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
97
146
  const sourceDropdownRef = useRef<HTMLDivElement>(null)
98
- const [mainLoopSaving, setMainLoopSaving] = useState(false)
99
- const [mainLoopError, setMainLoopError] = useState('')
100
- const [mainLoopNotice, setMainLoopNotice] = useState('')
101
147
  const [syncingHistory, setSyncingHistory] = useState(false)
102
148
  const [syncResult, setSyncResult] = useState('')
103
149
  const [renaming, setRenaming] = useState(false)
@@ -134,6 +180,46 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
134
180
  }, [fetchWalletBalance])
135
181
  useWs('wallets', fetchWalletBalance)
136
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])
137
223
 
138
224
  const visibleHeaderWidgets = useMemo(() => {
139
225
  const seen = new Set<string>()
@@ -145,6 +231,25 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
145
231
  })
146
232
  }, [headerWidgets])
147
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
+
148
253
  const handleHeaderWidgetClick = (widgetId: string) => {
149
254
  if (widgetId === 'wallet-status') {
150
255
  if (agent?.id) setWalletPanelAgentId(agent.id)
@@ -197,17 +302,17 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
197
302
  const handleDismissResumeHandle = async (e: React.MouseEvent) => {
198
303
  e.stopPropagation()
199
304
  try {
200
- await api('PUT', `/sessions/${session.id}`, {
305
+ await api('PUT', `/chats/${session.id}`, {
201
306
  claudeSessionId: null,
202
307
  codexThreadId: null,
203
308
  opencodeSessionId: null,
204
- delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
309
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
205
310
  })
206
311
  await loadSessions()
207
312
  } catch { /* best-effort */ }
208
313
  }
209
314
 
210
- const heartbeatSupported = (session.tools?.length ?? 0) > 0
315
+ const heartbeatSupported = (session.plugins?.length ?? 0) > 0
211
316
  const loopIsOngoing = appSettings.loopMode === 'ongoing'
212
317
  const { heartbeatEnabled, heartbeatIntervalSec, heartbeatExplicitOptIn } = useMemo(() => {
213
318
  // Resolve through the same cascade as the backend: settings → agent → session
@@ -254,13 +359,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
254
359
  }
255
360
  }, [appSettings, agent, session])
256
361
  const heartbeatWillRun = heartbeatEnabled && (loopIsOngoing || heartbeatExplicitOptIn)
257
- const isMainSession = session.name === '__main__'
258
- const missionState = session.mainLoopState || {}
259
- const missionPaused = missionState.paused === true
260
- const missionMode = missionState.autonomyMode === 'assist' ? 'assist' : 'autonomous'
261
- const missionStatus = missionState.status || 'idle'
262
- const missionMomentum = typeof missionState.momentumScore === 'number' ? missionState.momentumScore : null
263
- const missionEventsCount = missionState.pendingEvents?.length || 0
264
362
 
265
363
  const handleToggleHeartbeat = async () => {
266
364
  if (!heartbeatSupported || heartbeatSaving) return
@@ -270,10 +368,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
270
368
  if (session.agentId) {
271
369
  await api('PUT', `/agents/${session.agentId}`, { heartbeatEnabled: next })
272
370
  // Clear any stale session-level override so the agent value wins
273
- await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: null })
371
+ await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: null })
274
372
  await Promise.all([loadAgents(), loadSessions()])
275
373
  } else {
276
- await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: next })
374
+ await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: next })
277
375
  await loadSessions()
278
376
  }
279
377
  toast.success(`Heartbeat ${next ? 'enabled' : 'disabled'}`)
@@ -295,10 +393,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
295
393
  heartbeatEnabled: true,
296
394
  })
297
395
  // Clear stale session-level overrides
298
- await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
396
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
299
397
  await Promise.all([loadAgents(), loadSessions()])
300
398
  } else {
301
- await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
399
+ await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
302
400
  await loadSessions()
303
401
  }
304
402
  } finally {
@@ -306,63 +404,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
306
404
  }
307
405
  }
308
406
 
309
- const postMainLoopAction = async (action: string, extra?: Record<string, unknown>) => {
310
- if (!isMainSession || mainLoopSaving) return
311
- setMainLoopSaving(true)
312
- try {
313
- const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/sessions/${session.id}/main-loop`, {
314
- action,
315
- ...(extra || {}),
316
- })
317
- setMainLoopError('')
318
- if (action === 'nudge') {
319
- setMainLoopNotice(result?.deduped ? 'Nudge already queued.' : 'Nudge queued.')
320
- } else if (action === 'set_mode') {
321
- setMainLoopNotice(`Mode set to ${extra?.mode === 'assist' ? 'Assist' : 'Auto'}.`)
322
- } else {
323
- setMainLoopNotice('')
324
- }
325
- await loadSessions()
326
- } catch (err: unknown) {
327
- const message = err instanceof Error ? err.message : 'Failed to update mission controls.'
328
- setMainLoopError(message)
329
- } finally {
330
- setMainLoopSaving(false)
331
- }
332
- }
333
-
334
- const handleToggleMissionPause = () => {
335
- void postMainLoopAction(missionPaused ? 'resume' : 'pause')
336
- }
337
-
338
- const handleToggleMissionMode = () => {
339
- const nextMode = missionMode === 'autonomous' ? 'assist' : 'autonomous'
340
- void postMainLoopAction('set_mode', { mode: nextMode })
341
- }
342
-
343
- const handleNudgeMission = () => {
344
- void postMainLoopAction('nudge')
345
- }
346
-
347
- const handleSetMissionGoal = () => {
348
- if (!isMainSession) return
349
- const seededGoal = typeof missionState.goal === 'string' ? missionState.goal : ''
350
- const raw = window.prompt('Set mission goal', seededGoal)
351
- const goal = (raw || '').trim()
352
- if (!goal) return
353
- void postMainLoopAction('set_goal', { goal })
354
- }
355
-
356
- const handleClearMissionEvents = () => {
357
- if (!isMainSession || missionEventsCount <= 0) return
358
- void postMainLoopAction('clear_events')
359
- }
360
-
361
407
  const isOpenClawAgent = agent?.provider === 'openclaw'
362
- // Derive OpenClaw session key: agent sessions use "agent:<name>:main" convention
363
- const openclawSessionKey = isOpenClawAgent && agent
364
- ? `agent:${agent.name.toLowerCase().replace(/\s+/g, '-')}:main`
365
- : null
408
+ const openclawSessionKey = isOpenClawAgent ? buildOpenClawMainSessionKey(agent?.name) : null
366
409
 
367
410
  const handleSyncHistory = async () => {
368
411
  if (!openclawSessionKey || syncingHistory) return
@@ -473,7 +516,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
473
516
  const handleModelSwitch = async (nextProvider: ProviderType, nextModel: string) => {
474
517
  setModelSwitcherOpen(false)
475
518
  try {
476
- await api('PUT', `/sessions/${session.id}`, { provider: nextProvider, model: nextModel })
519
+ await api('PUT', `/chats/${session.id}`, { provider: nextProvider, model: nextModel })
477
520
  await loadSessions()
478
521
  } catch (err: unknown) {
479
522
  toast.error(err instanceof Error ? err.message : 'Failed to switch model')
@@ -490,35 +533,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
490
533
  }, [session.name, loadConnectors])
491
534
 
492
535
  useEffect(() => {
493
- setMainLoopError('')
494
- setMainLoopNotice('')
495
536
  setModelSwitcherOpen(false)
496
537
  }, [session.id])
497
538
 
498
- useEffect(() => {
499
- if (!mainLoopNotice) return
500
- const timer = setTimeout(() => setMainLoopNotice(''), 2500)
501
- return () => clearTimeout(timer)
502
- }, [mainLoopNotice])
503
-
504
- // Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
505
- const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
506
- const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
539
+ // Context bar shows for tools, memories, source filter, task links, resume handles, browser
540
+ const hasToolToggles = ((agent?.plugins?.length ?? 0) > 0) || ((session.plugins?.length ?? 0) > 0)
541
+ const hasMemoryLink = !!(agent && session.plugins?.includes('memory'))
507
542
  const hasSourceFilter = !!hasMultipleSources
508
- const hasContextBar = !!(isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
543
+ const hasContextBar = !!(hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
509
544
 
510
545
  return (
546
+ <>
511
547
  <header
512
548
  className="relative z-20 border-b border-white/[0.06] shrink-0"
513
549
  style={{
514
- 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%)',
515
551
  backdropFilter: 'blur(20px) saturate(1.4)',
516
552
  WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
517
553
  ...(mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : {}),
518
554
  }}
519
555
  >
520
556
  {/* Main row */}
521
- <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]">
522
558
  {/* Back button */}
523
559
  {onBack && (
524
560
  <IconButton onClick={onBack} aria-label="Go back" size="sm">
@@ -559,10 +595,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
559
595
  )}
560
596
 
561
597
  {/* Identity + metadata — fills center */}
562
- <div className="flex-1 min-w-0 flex items-center gap-3">
563
- {/* Name (row 1) + tools (row 2) */}
564
- <div className="flex flex-col gap-0.5 min-w-0 shrink">
565
- <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">
566
600
  {renaming && agent ? (
567
601
  <span ref={renameContainerRef} className="inline-flex items-center gap-2">
568
602
  <input
@@ -580,99 +614,90 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
580
614
  {renameSaving && <span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-accent-bright animate-spin shrink-0" />}
581
615
  {renameError && <span className="text-[10px] text-red-400 shrink-0">{renameError}</span>}
582
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>
583
634
  ) : (
584
- <span
585
- className={`font-display text-[15px] font-700 truncate tracking-[-0.02em] text-text${agent ? ' cursor-pointer hover:text-accent-bright transition-colors duration-200' : ''}`}
586
- onClick={agent ? startRename : undefined}
587
- title={agent ? 'Click to rename' : undefined}
588
- >{
589
- session.name === '__main__' ? 'Main Chat'
590
- : session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
591
- : session.name
592
- }</span>
635
+ <span className="font-display text-[16px] font-700 truncate tracking-[-0.02em] text-text">{session.name}</span>
593
636
  )}
594
637
  {connector && connectorMeta && (
595
638
  <span
596
- 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"
597
640
  style={{
598
641
  color: connectorMeta.color,
599
- backgroundColor: `${connectorMeta.color}10`,
600
- borderColor: `${connectorMeta.color}20`,
642
+ backgroundColor: `${connectorMeta.color}12`,
643
+ borderColor: `${connectorMeta.color}22`,
601
644
  }}
602
645
  title={`${connector.name} connector`}
603
646
  >
604
647
  <ConnectorPlatformIcon platform={connector.platform} size={10} />
605
- {connectorMeta.label}
648
+ <span className="truncate max-w-[140px]">{connectorMeta.label}</span>
606
649
  </span>
607
650
  )}
608
- {connector && connectorPresence && (() => {
609
- const lastAt = connectorPresence.lastMessageAt
610
- if (!lastAt) return (
611
- <span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-text-3/40">
612
- <span className="w-1.5 h-1.5 rounded-full bg-text-3/30" />
613
- Idle
614
- </span>
615
- )
616
- const ago = Date.now() - lastAt
617
- const isActive = ago < 5 * 60_000
618
- const isRecent = ago < 30 * 60_000
619
- const label = isActive ? 'Active' : isRecent ? `${Math.floor(ago / 60_000)}m ago` : 'Idle'
620
- const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/30'
621
- const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/40'
622
- return (
623
- <span className={`shrink-0 inline-flex items-center gap-1 text-[10px] ${textColor}`}>
624
- <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
625
- {label}
626
- </span>
627
- )
628
- })()}
629
- {agent?.isOrchestrator && (
630
- <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>
631
659
  )}
632
660
  {streaming && (
633
- <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>
634
665
  )}
635
666
  </div>
636
- {hasToolToggles && <ChatToolToggles session={session} />}
637
- </div>
638
-
639
- {/* Metadata tray: wallet · model · path · status */}
640
- <div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
641
- <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} />}
642
669
  {visibleHeaderWidgets.map((widget) => {
643
670
  const actionable = widget.id === 'wallet-status'
644
- const walletLabel = walletBalance !== null
645
- ? `${walletBalance.toFixed(3)} SOL`
671
+ const walletLabel = actionable
672
+ ? walletHeaderMeta.label
646
673
  : (widget.label || 'Wallet')
674
+ const widgetTitle = actionable
675
+ ? walletHeaderMeta.title
676
+ : widget.label
647
677
  return (
648
- <button
678
+ <HeaderChip
649
679
  key={widget.id}
650
- type="button"
651
680
  onClick={actionable ? () => handleHeaderWidgetClick(widget.id) : undefined}
652
- className={`inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] text-[11px] font-mono transition-colors ${
653
- actionable ? 'cursor-pointer text-text-3/45 hover:text-text-3/70 hover:bg-white/[0.04]' : 'cursor-default text-text-3/55'
654
- }`}
655
- title={actionable ? 'View wallet' : widget.label}
681
+ title={widgetTitle}
682
+ className={actionable ? 'text-text-3/80' : ''}
656
683
  >
657
684
  {actionable ? (
658
685
  <>
659
- <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">
660
687
  <rect x="2" y="6" width="20" height="14" rx="2" />
661
688
  <path d="M22 10H18a2 2 0 0 0 0 4h4" />
662
689
  </svg>
663
- {walletLabel}
690
+ <span className="truncate max-w-[120px]">{walletLabel}</span>
664
691
  </>
665
692
  ) : (
666
- widget.label
693
+ <span className="truncate max-w-[120px]">{widget.label}</span>
667
694
  )}
668
- </button>
695
+ </HeaderChip>
669
696
  )
670
697
  })}
671
- {visibleHeaderWidgets.length > 0 && (
672
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
673
- )}
674
698
  {modelName && (
675
699
  <div className="relative shrink-0" ref={modelSwitcherRef}>
700
+ <Tip label={`Switch model (${providerLabel})`}>
676
701
  <button
677
702
  type="button"
678
703
  onClick={() => {
@@ -680,16 +705,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
680
705
  setModelSwitcherOpen((o) => { if (!o) void loadProviders(); return !o })
681
706
  }}
682
707
  disabled={streaming}
683
- 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"
684
- title="Switch model"
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"
685
709
  >
686
- {modelName}
687
- <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">
688
715
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
689
716
  </svg>
690
717
  </button>
718
+ </Tip>
691
719
  {modelSwitcherOpen && (
692
- <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">
693
721
  <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
694
722
  <div className="flex flex-wrap gap-1.5 mb-3">
695
723
  {providers.map((p) => (
@@ -717,110 +745,108 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
717
745
  )}
718
746
  </div>
719
747
  )}
720
- <button
721
- 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
722
758
  onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
723
- 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"
724
- title={shortPath(session.cwd)}
759
+ title={workspaceLabel}
760
+ className="max-w-[min(44vw,220px)]"
725
761
  >
726
- <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">
727
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" />
728
764
  </svg>
729
- </button>
730
- {/* Live agent status */}
731
- {(() => {
732
- const liveStatus = agentStatus || (missionState.status ? {
733
- goal: missionState.goal ?? undefined,
734
- status: missionState.status ?? undefined,
735
- summary: missionState.summary ?? undefined,
736
- nextAction: missionState.nextAction ?? undefined,
737
- } : null)
738
- if (!liveStatus) return null
739
- const statusColors: Record<string, string> = {
740
- idle: 'bg-text-3/40', progress: 'bg-blue-500', blocked: 'bg-amber-400', ok: 'bg-emerald-400',
741
- }
742
- const dotColor = statusColors[liveStatus.status || ''] || 'bg-text-3/40'
743
- return (
744
- <>
745
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
746
- {liveStatus.status && (
747
- <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] text-[9px] font-700 uppercase tracking-wider ${
748
- liveStatus.status === 'blocked' ? 'bg-amber-400/12 text-amber-300'
749
- : liveStatus.status === 'ok' ? 'bg-emerald-400/12 text-emerald-400'
750
- : liveStatus.status === 'progress' ? 'bg-blue-500/12 text-blue-400'
751
- : 'bg-white/[0.03] text-text-3/50'
752
- }`}>
753
- <span className={`w-1 h-1 rounded-full ${dotColor}`} />
754
- {liveStatus.status}
755
- </span>
756
- )}
757
- {liveStatus.goal && (
758
- <span className="text-[10px] text-text-3/40 font-mono truncate max-w-[180px]" title={liveStatus.goal}>
759
- {liveStatus.goal}
760
- </span>
761
- )}
762
- {liveStatus.nextAction && (
763
- <>
764
- <span className="text-[9px] text-text-3/20 shrink-0">→</span>
765
- <span className="text-[10px] text-text-3/35 font-mono truncate max-w-[140px]" title={liveStatus.nextAction}>
766
- {liveStatus.nextAction}
767
- </span>
768
- </>
769
- )}
770
- </>
771
- )
772
- })()}
765
+ <span className="truncate">{mobile ? 'Workspace' : workspaceLabel}</span>
766
+ </HeaderChip>
767
+ </Tip>
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
+ )}
773
792
  </div>
774
793
  </div>
775
794
 
776
- {/* Heartbeat compound control */}
777
- {heartbeatSupported && (
778
- <div className="flex items-center rounded-[8px] shrink-0" style={{ background: 'rgba(255,255,255,0.025)' }}>
779
- <button
780
- onClick={handleToggleHeartbeat}
781
- disabled={heartbeatSaving}
782
- 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
783
- ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/60 hover:bg-white/[0.04]'}`}
784
- title={heartbeatWillRun ? 'Disable heartbeat' : 'Enable heartbeat'}
785
- >
786
- <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/30'}`} />
787
- HB
788
- {heartbeatEnabled && !loopIsOngoing && !heartbeatExplicitOptIn && (
789
- <span className="text-[9px] text-text-3/40">(bounded)</span>
790
- )}
791
- </button>
792
- <div className="relative" ref={hbDropdownRef}>
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'}>
793
800
  <button
794
- onClick={() => setHbDropdownOpen((o) => !o)}
801
+ onClick={handleToggleHeartbeat}
795
802
  disabled={heartbeatSaving}
796
- 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"
797
- title="Set heartbeat interval"
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]'}`}
798
806
  >
799
- <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
800
- <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
801
- <polyline points="6 9 12 15 18 9" />
802
- </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>
803
815
  </button>
804
- {hbDropdownOpen && (
805
- <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]">
806
- {[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
807
- <button
808
- key={sec}
809
- onClick={() => handleSelectHeartbeatInterval(sec)}
810
- className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
811
- ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
812
- >
813
- {formatDuration(sec)}
814
- </button>
815
- ))}
816
- </div>
817
- )}
816
+ </Tip>
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>
818
845
  </div>
819
- </div>
820
- )}
846
+ )}
821
847
 
822
- {/* Action buttons */}
823
- <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">
824
850
  {streaming && (
825
851
  <>
826
852
  <IconButton onClick={onStop} variant="danger" tooltip="Stop" aria-label="Stop generation" size="sm">
@@ -881,79 +907,29 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
881
907
  </IconButton>
882
908
  )}
883
909
  </div>
910
+ </div>
884
911
  </div>
885
912
 
886
- {/* Context bar: tools, mission controls, links */}
913
+ {/* Context bar: tools and links */}
887
914
  {hasContextBar && (
888
- <div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
889
- {isMainSession && (
890
- <>
891
- <button
892
- onClick={handleToggleMissionPause}
893
- disabled={mainLoopSaving}
894
- className={`flex items-center gap-1.5 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
895
- ${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'}`}
896
- title={missionPaused ? 'Resume mission' : 'Pause mission'}
897
- >
898
- <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
899
- {missionPaused ? 'Paused' : 'Live'}
900
- </button>
901
-
902
- <button
903
- onClick={handleToggleMissionMode}
904
- disabled={mainLoopSaving}
905
- className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
906
- ${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'}`}
907
- title="Toggle autonomy mode"
908
- >
909
- {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
910
- </button>
911
- <button
912
- onClick={handleNudgeMission}
913
- disabled={mainLoopSaving || missionPaused}
914
- 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"
915
- title="Run one tick"
916
- >
917
- Nudge
918
- </button>
919
- <button
920
- onClick={handleSetMissionGoal}
921
- disabled={mainLoopSaving}
922
- 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"
923
- title="Set mission goal"
924
- >
925
- Goal
926
- </button>
927
- {missionEventsCount > 0 && (
928
- <button
929
- onClick={handleClearMissionEvents}
930
- disabled={mainLoopSaving}
931
- 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"
932
- title="Clear pending events"
933
- >
934
- Events {missionEventsCount}
935
- </button>
936
- )}
937
- <span className="text-[9px] text-text-3/40 uppercase tracking-wider shrink-0">
938
- {missionStatus}{missionMomentum !== null ? ` · ${missionMomentum}` : ''}
939
- </span>
940
- {mainLoopError && <span className="text-[9px] text-red-300/80 truncate max-w-[240px]" title={mainLoopError}>{mainLoopError}</span>}
941
- {mainLoopNotice && <span className="text-[9px] text-emerald-300/80 truncate max-w-[200px]" title={mainLoopNotice}>{mainLoopNotice}</span>}
942
- </>
943
- )}
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">
944
917
  {hasMemoryLink && (
918
+ <Tip label="View agent memories">
945
919
  <button
946
920
  onClick={() => { setMemoryAgentFilter(session.agentId!); setActiveView('memory'); setSidebarOpen(true) }}
947
- 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"
948
922
  >
949
923
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
950
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" />
951
925
  </svg>
952
926
  Memories
953
927
  </button>
928
+ </Tip>
954
929
  )}
955
930
  {hasSourceFilter && onConnectorFilterChange && connectorSources && (
956
931
  <div className="relative shrink-0" ref={sourceDropdownRef}>
932
+ <Tip label="Filter messages by source connector">
957
933
  <button
958
934
  onClick={() => setSourceDropdownOpen((o) => !o)}
959
935
  className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600 shrink-0 ${
@@ -961,7 +937,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
961
937
  ? 'bg-accent-soft/60 text-accent-bright/80 hover:bg-accent-soft'
962
938
  : 'bg-white/[0.03] text-text-3/50 hover:bg-white/[0.06] hover:text-text-3/70'
963
939
  }`}
964
- title="Filter by message source"
965
940
  >
966
941
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
967
942
  <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
@@ -973,8 +948,9 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
973
948
  <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
974
949
  </svg>
975
950
  </button>
951
+ </Tip>
976
952
  {sourceDropdownOpen && (
977
- <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)]">
978
954
  <button
979
955
  onClick={() => { onConnectorFilterChange(null); setSourceDropdownOpen(false) }}
980
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 ${
@@ -1005,11 +981,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1005
981
  )}
1006
982
  {isOpenClawAgent && openclawSessionKey && (
1007
983
  <>
984
+ <Tip label="Sync chat history from OpenClaw gateway">
1008
985
  <button
1009
986
  onClick={handleSyncHistory}
1010
987
  disabled={syncingHistory}
1011
988
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-indigo-500/8 hover:bg-indigo-500/12 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600 text-indigo-400 shrink-0"
1012
- title="Sync from gateway"
1013
989
  >
1014
990
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
1015
991
  <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" />
@@ -1017,10 +993,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1017
993
  </svg>
1018
994
  {syncingHistory ? 'Syncing...' : 'Sync'}
1019
995
  </button>
996
+ </Tip>
1020
997
  {syncResult && <span className="text-[9px] text-emerald-300/80 shrink-0">{syncResult}</span>}
1021
998
  </>
1022
999
  )}
1023
1000
  {linkedTask && (
1001
+ <Tip label="View linked task">
1024
1002
  <button
1025
1003
  onClick={() => setActiveView('tasks')}
1026
1004
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-amber-500/8 hover:bg-amber-500/12 transition-colors cursor-pointer text-[10px] font-600 text-amber-500 shrink-0"
@@ -1030,37 +1008,40 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1030
1008
  </svg>
1031
1009
  <span className="truncate max-w-[160px]">{linkedTask.title}</span>
1032
1010
  </button>
1011
+ </Tip>
1033
1012
  )}
1034
1013
  {resumeHandle && (
1035
1014
  <div className="flex items-center rounded-[7px] bg-white/[0.03] group/resume shrink-0">
1015
+ <Tip label="Copy CLI resume command">
1036
1016
  <button
1037
1017
  onClick={handleCopySessionId}
1038
- className="flex items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
1039
- title="Copy resume command"
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"
1040
1019
  >
1041
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">
1042
1021
  <path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
1043
1022
  </svg>
1044
- <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)]">
1045
1024
  {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
1046
1025
  </span>
1047
1026
  </button>
1027
+ </Tip>
1028
+ <Tip label="Dismiss resume handle">
1048
1029
  <button
1049
1030
  onClick={handleDismissResumeHandle}
1050
- className="px-1 py-1 rounded-r-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer opacity-0 group-hover/resume:opacity-100"
1051
- title="Dismiss"
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"
1052
1032
  >
1053
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">
1054
1034
  <path d="M4 4l8 8M12 4l-8 8" />
1055
1035
  </svg>
1056
1036
  </button>
1037
+ </Tip>
1057
1038
  </div>
1058
1039
  )}
1059
1040
  {browserActive && (
1041
+ <Tip label="Close the browser session">
1060
1042
  <button
1061
1043
  onClick={onStopBrowser}
1062
1044
  className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-accent-bright/8 hover:bg-red-500/12 transition-colors cursor-pointer group text-[10px] font-600 shrink-0"
1063
- title="Stop browser"
1064
1045
  >
1065
1046
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright group-hover:text-red-400">
1066
1047
  <rect x="3" y="3" width="18" height="14" rx="2" /><path d="M3 9h18" />
@@ -1070,9 +1051,13 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
1070
1051
  <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
1071
1052
  </svg>
1072
1053
  </button>
1054
+ </Tip>
1073
1055
  )}
1074
1056
  </div>
1057
+ </div>
1075
1058
  )}
1059
+
1076
1060
  </header>
1061
+ </>
1077
1062
  )
1078
1063
  }