@swarmclawai/swarmclaw 0.6.0 → 0.6.2

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 (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. package/src/types/index.ts +32 -2
@@ -14,7 +14,9 @@ import {
14
14
  getSessionConnector,
15
15
  } from '@/components/shared/connector-platform-icon'
16
16
  import { AgentAvatar } from '@/components/agents/agent-avatar'
17
+ import { ModelCombobox } from '@/components/shared/model-combobox'
17
18
  import { toast } from 'sonner'
19
+ import type { ProviderType } from '@/types'
18
20
 
19
21
  function shortPath(p: string): string {
20
22
  return (p || '').replace(/^\/Users\/\w+/, '~')
@@ -49,9 +51,11 @@ interface Props {
49
51
  onVoiceToggle?: () => void
50
52
  voiceActive?: boolean
51
53
  voiceSupported?: boolean
54
+ heartbeatHistoryOpen?: boolean
55
+ onToggleHeartbeatHistory?: () => void
52
56
  }
53
57
 
54
- export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported }: Props) {
58
+ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, heartbeatHistoryOpen, onToggleHeartbeatHistory }: Props) {
55
59
  const ttsEnabled = useChatStore((s) => s.ttsEnabled)
56
60
  const toggleTts = useChatStore((s) => s.toggleTts)
57
61
  const soundEnabled = useChatStore((s) => s.soundEnabled)
@@ -72,12 +76,15 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
72
76
  const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
73
77
  const connectors = useAppStore((s) => s.connectors)
74
78
  const loadConnectors = useAppStore((s) => s.loadConnectors)
75
- const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
76
79
  const agent = session.agentId ? agents[session.agentId] : null
77
80
  const connector = getSessionConnector(session, connectors)
78
81
  const connectorMeta = connector ? CONNECTOR_PLATFORM_META[connector.platform] : null
79
82
  const connectorPresence = connector?.presence
83
+ const providers = useAppStore((s) => s.providers)
84
+ const loadProviders = useAppStore((s) => s.loadProviders)
80
85
  const modelName = session.model || agent?.model || ''
86
+ const [modelSwitcherOpen, setModelSwitcherOpen] = useState(false)
87
+ const modelSwitcherRef = useRef<HTMLDivElement>(null)
81
88
  const [copied, setCopied] = useState(false)
82
89
  const [heartbeatSaving, setHeartbeatSaving] = useState(false)
83
90
  const [hbDropdownOpen, setHbDropdownOpen] = useState(false)
@@ -87,6 +94,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
87
94
  const [mainLoopNotice, setMainLoopNotice] = useState('')
88
95
  const [syncingHistory, setSyncingHistory] = useState(false)
89
96
  const [syncResult, setSyncResult] = useState('')
97
+ const [renaming, setRenaming] = useState(false)
98
+ const [renameDraft, setRenameDraft] = useState('')
99
+ const [renameSaving, setRenameSaving] = useState(false)
100
+ const [renameError, setRenameError] = useState('')
101
+ const renameInputRef = useRef<HTMLInputElement>(null)
102
+ const renameContainerRef = useRef<HTMLSpanElement>(null)
90
103
 
91
104
  // Find linked task for this session
92
105
  const linkedTask = useMemo(() => {
@@ -128,6 +141,19 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
128
141
  setTimeout(() => setCopied(false), 2000)
129
142
  }
130
143
 
144
+ const handleDismissResumeHandle = async (e: React.MouseEvent) => {
145
+ e.stopPropagation()
146
+ try {
147
+ await api('PUT', `/sessions/${session.id}`, {
148
+ claudeSessionId: null,
149
+ codexThreadId: null,
150
+ opencodeSessionId: null,
151
+ delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
152
+ })
153
+ await loadSessions()
154
+ } catch { /* best-effort */ }
155
+ }
156
+
131
157
  const heartbeatSupported = (session.tools?.length ?? 0) > 0
132
158
  const loopIsOngoing = appSettings.loopMode === 'ongoing'
133
159
  const { heartbeatEnabled, heartbeatIntervalSec, heartbeatExplicitOptIn } = useMemo(() => {
@@ -316,6 +342,54 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
316
342
  return () => clearTimeout(timer)
317
343
  }, [syncResult])
318
344
 
345
+ const startRename = () => {
346
+ if (!agent) return
347
+ setRenameDraft(agent.name)
348
+ setRenameError('')
349
+ setRenaming(true)
350
+ requestAnimationFrame(() => {
351
+ renameInputRef.current?.focus()
352
+ renameInputRef.current?.select()
353
+ })
354
+ }
355
+
356
+ const cancelRename = () => {
357
+ setRenaming(false)
358
+ setRenameDraft('')
359
+ setRenameError('')
360
+ }
361
+
362
+ const commitRename = async () => {
363
+ if (!agent || renameSaving) return
364
+ const trimmed = renameDraft.trim()
365
+ if (!trimmed || trimmed === agent.name) {
366
+ cancelRename()
367
+ return
368
+ }
369
+ setRenameSaving(true)
370
+ setRenameError('')
371
+ try {
372
+ await api('PUT', `/agents/${agent.id}`, { name: trimmed })
373
+ await loadAgents()
374
+ setRenaming(false)
375
+ } catch (err: unknown) {
376
+ setRenameError(err instanceof Error ? err.message : 'Rename failed')
377
+ } finally {
378
+ setRenameSaving(false)
379
+ }
380
+ }
381
+
382
+ useEffect(() => {
383
+ if (!renaming) return
384
+ const handler = (e: PointerEvent) => {
385
+ if (renameContainerRef.current && !renameContainerRef.current.contains(e.target as Node)) {
386
+ cancelRename()
387
+ }
388
+ }
389
+ document.addEventListener('pointerdown', handler, true)
390
+ return () => document.removeEventListener('pointerdown', handler, true)
391
+ }, [renaming])
392
+
319
393
  useEffect(() => {
320
394
  if (!hbDropdownOpen) return
321
395
  const handler = (e: MouseEvent) => {
@@ -325,6 +399,28 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
325
399
  return () => document.removeEventListener('mousedown', handler)
326
400
  }, [hbDropdownOpen])
327
401
 
402
+ useEffect(() => {
403
+ if (!modelSwitcherOpen) return
404
+ const handler = (e: MouseEvent) => {
405
+ if (modelSwitcherRef.current && !modelSwitcherRef.current.contains(e.target as Node)) setModelSwitcherOpen(false)
406
+ }
407
+ document.addEventListener('mousedown', handler)
408
+ return () => document.removeEventListener('mousedown', handler)
409
+ }, [modelSwitcherOpen])
410
+
411
+ const handleModelSwitch = async (nextProvider: ProviderType, nextModel: string) => {
412
+ setModelSwitcherOpen(false)
413
+ try {
414
+ await api('PUT', `/sessions/${session.id}`, { provider: nextProvider, model: nextModel })
415
+ await loadSessions()
416
+ } catch (err: unknown) {
417
+ toast.error(err instanceof Error ? err.message : 'Failed to switch model')
418
+ }
419
+ }
420
+
421
+ const currentProviderInfo = providers.find((p) => p.id === session.provider)
422
+ const currentModels = currentProviderInfo?.models || []
423
+
328
424
  useEffect(() => {
329
425
  if (session.name.startsWith('connector:')) {
330
426
  void loadConnectors()
@@ -334,6 +430,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
334
430
  useEffect(() => {
335
431
  setMainLoopError('')
336
432
  setMainLoopNotice('')
433
+ setModelSwitcherOpen(false)
337
434
  }, [session.id])
338
435
 
339
436
  useEffect(() => {
@@ -342,53 +439,122 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
342
439
  return () => clearTimeout(timer)
343
440
  }, [mainLoopNotice])
344
441
 
442
+ // Context bar shows for tools, mission controls, memories, task links, resume handles, browser
443
+ const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
444
+ const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
445
+ const hasContextBar = !!(hasToolToggles || isMainSession || hasMemoryLink || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
446
+
345
447
  return (
346
- <header className="relative z-20 flex flex-col border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0"
347
- style={mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : undefined}>
348
- <div className="flex items-center gap-3 px-5 py-3 min-h-[56px]">
448
+ <header
449
+ className="relative z-20 border-b border-white/[0.06] shrink-0"
450
+ style={{
451
+ background: 'linear-gradient(180deg, rgba(var(--rgb-bg, 15,15,26), 0.95) 0%, rgba(var(--rgb-bg, 15,15,26), 0.88) 100%)',
452
+ backdropFilter: 'blur(20px) saturate(1.4)',
453
+ WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
454
+ ...(mobile ? { paddingTop: 'max(12px, env(safe-area-inset-top))' } : {}),
455
+ }}
456
+ >
457
+ {/* Main row */}
458
+ <div className="flex items-center gap-2 px-3.5 py-1.5 min-h-[48px]">
459
+ {/* Back button */}
349
460
  {onBack && (
350
- <IconButton onClick={onBack} aria-label="Go back">
351
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
461
+ <IconButton onClick={onBack} aria-label="Go back" size="sm">
462
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
352
463
  <polyline points="15 18 9 12 15 6" />
353
464
  </svg>
354
465
  </IconButton>
355
466
  )}
356
- <div className="flex-1 min-w-0">
357
- <div className="flex items-center gap-2.5">
358
- {agent && <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />}
359
- <span className="font-display text-[16px] font-600 block truncate tracking-[-0.02em]">{
360
- session.name === '__main__' ? 'Main Chat'
361
- : session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
362
- : session.name
363
- }</span>
467
+
468
+ {/* Avatar */}
469
+ {agent && (
470
+ <div className="relative shrink-0">
471
+ {streaming && (
472
+ <div
473
+ className="absolute -inset-[3px] rounded-full opacity-40"
474
+ style={{
475
+ background: 'conic-gradient(from 0deg, var(--color-accent-bright), transparent 120deg, transparent 240deg, var(--color-accent-bright))',
476
+ animation: 'spin 2.5s linear infinite',
477
+ filter: 'blur(3px)',
478
+ }}
479
+ />
480
+ )}
481
+ <div
482
+ className="relative rounded-full"
483
+ style={{
484
+ padding: 2,
485
+ background: streaming
486
+ ? 'conic-gradient(from 0deg, var(--color-accent-bright), transparent 120deg, transparent 240deg, var(--color-accent-bright))'
487
+ : 'linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.03))',
488
+ animation: streaming ? 'spin 2.5s linear infinite' : undefined,
489
+ }}
490
+ >
491
+ <div className="rounded-full bg-bg">
492
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={hasContextBar ? 44 : 34} />
493
+ </div>
494
+ </div>
495
+ </div>
496
+ )}
497
+
498
+ {/* Identity + metadata — fills center */}
499
+ <div className="flex-1 min-w-0 flex items-center gap-3">
500
+ {/* Name + inline badges */}
501
+ <div className="flex items-center gap-2 min-w-0 shrink">
502
+ {renaming && agent ? (
503
+ <span ref={renameContainerRef} className="inline-flex items-center gap-2">
504
+ <input
505
+ ref={renameInputRef}
506
+ value={renameDraft}
507
+ onChange={(e) => setRenameDraft(e.target.value)}
508
+ onKeyDown={(e) => {
509
+ if (e.key === 'Enter') void commitRename()
510
+ if (e.key === 'Escape') cancelRename()
511
+ }}
512
+ disabled={renameSaving}
513
+ className="font-display text-[15px] font-700 tracking-[-0.02em] bg-transparent border-b border-accent-bright/40 outline-none text-text px-0 py-0 w-[180px]"
514
+ style={{ fontFamily: 'inherit' }}
515
+ />
516
+ {renameSaving && <span className="w-3 h-3 rounded-full border-2 border-text-3/30 border-t-accent-bright animate-spin shrink-0" />}
517
+ {renameError && <span className="text-[10px] text-red-400 shrink-0">{renameError}</span>}
518
+ </span>
519
+ ) : (
520
+ <span
521
+ className={`font-display text-[15px] font-700 truncate tracking-[-0.02em] text-text${agent ? ' cursor-pointer hover:text-accent-bright transition-colors duration-200' : ''}`}
522
+ onClick={agent ? startRename : undefined}
523
+ title={agent ? 'Click to rename' : undefined}
524
+ >{
525
+ session.name === '__main__' ? 'Main Chat'
526
+ : session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
527
+ : session.name
528
+ }</span>
529
+ )}
364
530
  {connector && connectorMeta && (
365
531
  <span
366
- className="shrink-0 inline-flex items-center gap-1.5 px-2 py-0.5 rounded-[7px] border text-[10px] font-700 uppercase tracking-wider"
532
+ 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"
367
533
  style={{
368
534
  color: connectorMeta.color,
369
- backgroundColor: `${connectorMeta.color}1A`,
370
- borderColor: `${connectorMeta.color}33`,
535
+ backgroundColor: `${connectorMeta.color}10`,
536
+ borderColor: `${connectorMeta.color}20`,
371
537
  }}
372
538
  title={`${connector.name} connector`}
373
539
  >
374
- <ConnectorPlatformIcon platform={connector.platform} size={11} />
540
+ <ConnectorPlatformIcon platform={connector.platform} size={10} />
375
541
  {connectorMeta.label}
376
542
  </span>
377
543
  )}
378
544
  {connector && connectorPresence && (() => {
379
545
  const lastAt = connectorPresence.lastMessageAt
380
546
  if (!lastAt) return (
381
- <span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-text-3/50">
382
- <span className="w-1.5 h-1.5 rounded-full bg-text-3/40" />
383
- Inactive
547
+ <span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-text-3/40">
548
+ <span className="w-1.5 h-1.5 rounded-full bg-text-3/30" />
549
+ Idle
384
550
  </span>
385
551
  )
386
552
  const ago = Date.now() - lastAt
387
553
  const isActive = ago < 5 * 60_000
388
554
  const isRecent = ago < 30 * 60_000
389
- const label = isActive ? 'Active' : isRecent ? `${Math.floor(ago / 60_000)}m ago` : 'Inactive'
390
- const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/40'
391
- const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/50'
555
+ const label = isActive ? 'Active' : isRecent ? `${Math.floor(ago / 60_000)}m ago` : 'Idle'
556
+ const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/30'
557
+ const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/40'
392
558
  return (
393
559
  <span className={`shrink-0 inline-flex items-center gap-1 text-[10px] ${textColor}`}>
394
560
  <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
@@ -396,290 +562,305 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
396
562
  </span>
397
563
  )
398
564
  })()}
399
- {session.provider && session.provider !== 'claude-cli' && (
400
- <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-accent-soft text-accent-bright text-[10px] font-700 uppercase tracking-wider">
401
- {providerLabel}
402
- </span>
403
- )}
404
565
  {agent?.isOrchestrator && (
405
- <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-[#F59E0B]/10 text-[#F59E0B] text-[10px] font-700 uppercase tracking-wider">
406
- Orchestrator
407
- </span>
566
+ <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>
408
567
  )}
409
- {session.tools?.length ? (
410
- <span className="shrink-0 px-2.5 py-0.5 rounded-[7px] bg-emerald-500/10 text-emerald-400 text-[10px] font-700 uppercase tracking-wider">
411
- Tools
412
- </span>
413
- ) : null}
414
568
  {streaming && (
415
569
  <span className="shrink-0 w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
416
570
  )}
417
571
  </div>
418
- <div className="flex items-center gap-2 mt-0.5">
419
- <span className="text-[11px] text-text-3/60 font-mono block truncate">{shortPath(session.cwd)}</span>
572
+
573
+ {/* Metadata tray: model · usage · path · status */}
574
+ <div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
575
+ <span className="text-text-3/10 text-[10px] select-none shrink-0">/</span>
420
576
  {modelName && (
421
- <>
422
- <span className="text-[11px] text-text-3/60">·</span>
423
- <span className="text-[11px] text-text-3/50 font-mono truncate shrink-0">{modelName}</span>
424
- {session.conversationTone && session.conversationTone !== 'neutral' && (() => {
425
- const toneColors: Record<string, string> = {
426
- formal: 'bg-[#3B82F6]',
427
- casual: 'bg-emerald-400',
428
- empathetic: 'bg-purple-400',
429
- technical: 'bg-[#F59E0B]',
430
- }
431
- const color = toneColors[session.conversationTone] || ''
432
- return color ? (
433
- <span
434
- className={`w-2 h-2 rounded-full shrink-0 ${color}`}
435
- title={`Tone: ${session.conversationTone}`}
577
+ <div className="relative shrink-0" ref={modelSwitcherRef}>
578
+ <button
579
+ type="button"
580
+ onClick={() => {
581
+ if (streaming) return
582
+ setModelSwitcherOpen((o) => { if (!o) void loadProviders(); return !o })
583
+ }}
584
+ disabled={streaming}
585
+ 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"
586
+ title="Switch model"
587
+ >
588
+ {modelName}
589
+ <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-30">
590
+ <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
591
+ </svg>
592
+ </button>
593
+ {modelSwitcherOpen && (
594
+ <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">
595
+ <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
596
+ <div className="flex flex-wrap gap-1.5 mb-3">
597
+ {providers.map((p) => (
598
+ <button
599
+ key={p.id}
600
+ type="button"
601
+ onClick={() => { if (p.id !== session.provider) void handleModelSwitch(p.id, p.models[0] || '') }}
602
+ className={`px-2.5 py-1 rounded-[7px] text-[11px] font-600 border-none cursor-pointer transition-colors
603
+ ${p.id === session.provider ? 'bg-accent-bright/15 text-accent-bright' : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08]'}`}
604
+ >
605
+ {PROVIDER_LABELS[p.id] || p.id}
606
+ </button>
607
+ ))}
608
+ </div>
609
+ <div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Model</div>
610
+ <ModelCombobox
611
+ providerId={session.provider}
612
+ value={modelName}
613
+ onChange={(m) => void handleModelSwitch(session.provider, m)}
614
+ models={currentModels}
615
+ defaultModels={currentProviderInfo?.defaultModels}
616
+ className="px-2.5 py-1.5 rounded-[7px] text-[12px] font-mono bg-white/[0.04] hover:bg-white/[0.06] transition-colors"
436
617
  />
437
- ) : null
438
- })()}
439
- </>
618
+ </div>
619
+ )}
620
+ </div>
440
621
  )}
441
622
  {lastUsage && !streaming && (
442
623
  <>
443
- <span className="text-[11px] text-text-3/60">·</span>
624
+ <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
444
625
  <UsageBadge {...lastUsage} />
445
626
  </>
446
627
  )}
447
- </div>
448
- {(() => {
449
- const liveStatus = agentStatus || (missionState.status ? {
450
- goal: missionState.goal ?? undefined,
451
- status: missionState.status ?? undefined,
452
- summary: missionState.summary ?? undefined,
453
- nextAction: missionState.nextAction ?? undefined,
454
- } : null)
455
- if (!liveStatus) return null
456
- const statusColors: Record<string, string> = {
457
- idle: 'bg-text-3/40',
458
- progress: 'bg-[#3B82F6]',
459
- blocked: 'bg-amber-400',
460
- ok: 'bg-emerald-400',
461
- }
462
- const dotColor = statusColors[liveStatus.status || ''] || 'bg-text-3/40'
463
- return (
464
- <div className="flex items-center gap-2 mt-0.5">
465
- {liveStatus.goal && (
466
- <span className="text-[10px] text-text-3/60 font-mono truncate max-w-[240px]" title={liveStatus.goal}>
467
- {liveStatus.goal}
468
- </span>
469
- )}
470
- {liveStatus.status && (
471
- <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-wider ${
472
- liveStatus.status === 'blocked' ? 'bg-amber-400/15 text-amber-300'
473
- : liveStatus.status === 'ok' ? 'bg-emerald-400/15 text-emerald-400'
474
- : liveStatus.status === 'progress' ? 'bg-[#3B82F6]/15 text-[#60A5FA]'
475
- : 'bg-white/[0.04] text-text-3/60'
476
- }`}>
477
- <span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
478
- {liveStatus.status}
479
- </span>
480
- )}
481
- {liveStatus.nextAction && (
482
- <>
483
- <span className="text-[10px] text-text-3/40">→</span>
484
- <span className="text-[10px] text-text-3/50 font-mono truncate max-w-[200px]" title={liveStatus.nextAction}>
485
- {liveStatus.nextAction}
628
+ <button
629
+ type="button"
630
+ onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
631
+ 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"
632
+ title={shortPath(session.cwd)}
633
+ >
634
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
635
+ <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" />
636
+ </svg>
637
+ </button>
638
+ {/* Live agent status */}
639
+ {(() => {
640
+ const liveStatus = agentStatus || (missionState.status ? {
641
+ goal: missionState.goal ?? undefined,
642
+ status: missionState.status ?? undefined,
643
+ summary: missionState.summary ?? undefined,
644
+ nextAction: missionState.nextAction ?? undefined,
645
+ } : null)
646
+ if (!liveStatus) return null
647
+ const statusColors: Record<string, string> = {
648
+ idle: 'bg-text-3/40', progress: 'bg-blue-500', blocked: 'bg-amber-400', ok: 'bg-emerald-400',
649
+ }
650
+ const dotColor = statusColors[liveStatus.status || ''] || 'bg-text-3/40'
651
+ return (
652
+ <>
653
+ <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
654
+ {liveStatus.status && (
655
+ <span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[4px] text-[9px] font-700 uppercase tracking-wider ${
656
+ liveStatus.status === 'blocked' ? 'bg-amber-400/12 text-amber-300'
657
+ : liveStatus.status === 'ok' ? 'bg-emerald-400/12 text-emerald-400'
658
+ : liveStatus.status === 'progress' ? 'bg-blue-500/12 text-blue-400'
659
+ : 'bg-white/[0.03] text-text-3/50'
660
+ }`}>
661
+ <span className={`w-1 h-1 rounded-full ${dotColor}`} />
662
+ {liveStatus.status}
486
663
  </span>
487
- </>
488
- )}
489
- </div>
490
- )
491
- })()}
664
+ )}
665
+ {liveStatus.goal && (
666
+ <span className="text-[10px] text-text-3/40 font-mono truncate max-w-[180px]" title={liveStatus.goal}>
667
+ {liveStatus.goal}
668
+ </span>
669
+ )}
670
+ {liveStatus.nextAction && (
671
+ <>
672
+ <span className="text-[9px] text-text-3/20 shrink-0">→</span>
673
+ <span className="text-[10px] text-text-3/35 font-mono truncate max-w-[140px]" title={liveStatus.nextAction}>
674
+ {liveStatus.nextAction}
675
+ </span>
676
+ </>
677
+ )}
678
+ </>
679
+ )
680
+ })()}
681
+ </div>
492
682
  </div>
493
- <div className="flex gap-1.5">
683
+
684
+ {/* Heartbeat compound control */}
685
+ {heartbeatSupported && (
686
+ <div className="flex items-center rounded-[8px] shrink-0" style={{ background: 'rgba(255,255,255,0.025)' }}>
687
+ <button
688
+ onClick={handleToggleHeartbeat}
689
+ disabled={heartbeatSaving}
690
+ 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
691
+ ${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/60 hover:bg-white/[0.04]'}`}
692
+ title={heartbeatWillRun ? 'Disable heartbeat' : 'Enable heartbeat'}
693
+ >
694
+ <span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/30'}`} />
695
+ HB
696
+ {heartbeatEnabled && !loopIsOngoing && !heartbeatExplicitOptIn && (
697
+ <span className="text-[9px] text-text-3/40">(bounded)</span>
698
+ )}
699
+ </button>
700
+ <div className="relative" ref={hbDropdownRef}>
701
+ <button
702
+ onClick={() => setHbDropdownOpen((o) => !o)}
703
+ disabled={heartbeatSaving}
704
+ 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"
705
+ title="Set heartbeat interval"
706
+ >
707
+ <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
708
+ <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
709
+ <polyline points="6 9 12 15 18 9" />
710
+ </svg>
711
+ </button>
712
+ {hbDropdownOpen && (
713
+ <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]">
714
+ {[1800, 3600, 7200, 21600, 43200].map((sec) => (
715
+ <button
716
+ key={sec}
717
+ onClick={() => handleSelectHeartbeatInterval(sec)}
718
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
719
+ ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
720
+ >
721
+ {formatDuration(sec)}
722
+ </button>
723
+ ))}
724
+ </div>
725
+ )}
726
+ </div>
727
+ </div>
728
+ )}
729
+
730
+ {/* Action buttons */}
731
+ <div className="flex items-center shrink-0">
494
732
  {streaming && (
495
- <IconButton onClick={onStop} variant="danger" aria-label="Stop generation">
496
- <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
497
- <rect x="6" y="6" width="12" height="12" rx="2" />
498
- </svg>
499
- </IconButton>
500
- )}
501
- {agent && (
502
- <IconButton onClick={() => setInspectorOpen(!inspectorOpen)} active={inspectorOpen} aria-label="Toggle inspector panel">
503
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
504
- <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
505
- <circle cx="12" cy="12" r="3" />
506
- </svg>
507
- </IconButton>
733
+ <>
734
+ <IconButton onClick={onStop} variant="danger" tooltip="Stop" aria-label="Stop generation" size="sm">
735
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
736
+ <rect x="6" y="6" width="12" height="12" rx="2" />
737
+ </svg>
738
+ </IconButton>
739
+ <div className="w-px h-3.5 bg-white/[0.06] mx-0.5" />
740
+ </>
508
741
  )}
509
- <IconButton onClick={() => setDebugOpen(!debugOpen)} active={debugOpen} aria-label="Toggle debug panel">
510
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
511
- <path d="M12 20V10" />
512
- <path d="M18 20V4" />
513
- <path d="M6 20v-4" />
742
+ <IconButton onClick={toggleSound} active={soundEnabled} tooltip="Notifications" aria-label="Toggle sound" size="sm">
743
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
744
+ <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
745
+ <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
514
746
  </svg>
515
747
  </IconButton>
516
- <IconButton onClick={toggleSound} active={soundEnabled} aria-label="Toggle sound notifications">
517
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
518
- <path d="M18 8A6 6 0 0 1 18 16" />
519
- <path d="M13 2L8 7H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4l5 5V2z" />
520
- </svg>
521
- </IconButton>
522
- <IconButton onClick={toggleTts} active={ttsEnabled} aria-label="Toggle text-to-speech">
523
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
748
+ <IconButton onClick={toggleTts} active={ttsEnabled} tooltip="Read aloud" aria-label="Toggle TTS" size="sm">
749
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
524
750
  <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
525
751
  <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
526
752
  </svg>
527
753
  </IconButton>
528
754
  {voiceSupported && onVoiceToggle && (
529
- <IconButton onClick={onVoiceToggle} active={voiceActive} aria-label="Toggle voice conversation">
530
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
755
+ <IconButton onClick={onVoiceToggle} active={voiceActive} tooltip="Voice mode" aria-label="Toggle voice" size="sm">
756
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
531
757
  <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
532
758
  <path d="M19 10v2a7 7 0 0 1-14 0v-2" />
533
759
  <line x1="12" x2="12" y1="19" y2="22" />
534
760
  </svg>
535
761
  </IconButton>
536
762
  )}
537
- <IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} aria-label="Chat menu">
538
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
539
- <circle cx="12" cy="6" r="1" />
540
- <circle cx="12" cy="12" r="1" />
541
- <circle cx="12" cy="18" r="1" />
763
+ {agent?.heartbeatEnabled && onToggleHeartbeatHistory && (
764
+ <IconButton onClick={onToggleHeartbeatHistory} active={heartbeatHistoryOpen} tooltip="Heartbeat history" aria-label="Toggle heartbeat history" size="sm">
765
+ <svg width="14" height="14" viewBox="0 0 24 24" fill={heartbeatHistoryOpen ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
766
+ <path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
767
+ </svg>
768
+ </IconButton>
769
+ )}
770
+ <div className="w-px h-3.5 bg-white/[0.06] mx-0.5" />
771
+ <IconButton onClick={() => setDebugOpen(!debugOpen)} active={debugOpen} tooltip="Debug" aria-label="Toggle debug panel" size="sm">
772
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
773
+ <path d="M12 20V10" /><path d="M18 20V4" /><path d="M6 20v-4" />
542
774
  </svg>
543
775
  </IconButton>
776
+ {(!agent || mobile) && (
777
+ <IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} tooltip="Menu" aria-label="Chat menu" size="sm">
778
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
779
+ <circle cx="12" cy="6" r="1" /><circle cx="12" cy="12" r="1" /><circle cx="12" cy="18" r="1" />
780
+ </svg>
781
+ </IconButton>
782
+ )}
783
+ {agent && (
784
+ <IconButton onClick={() => setInspectorOpen(!inspectorOpen)} active={inspectorOpen} tooltip="Settings" aria-label="Toggle inspector" size="sm">
785
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
786
+ <path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
787
+ <circle cx="12" cy="12" r="3" />
788
+ </svg>
789
+ </IconButton>
790
+ )}
544
791
  </div>
545
792
  </div>
546
793
 
547
- {/* Sub-bar: tools toggle + agent memories + task link + CLI session ID + browser */}
548
- {(agent || linkedTask || resumeHandle || browserActive || session.tools?.length || isMainSession) && (
549
- <div className="flex items-center gap-3 px-5 pb-2.5 -mt-1">
550
- {(((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)) && (
551
- <ChatToolToggles session={session} />
552
- )}
553
- {heartbeatSupported && (
554
- <>
555
- <button
556
- onClick={handleToggleHeartbeat}
557
- disabled={heartbeatSaving}
558
- className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
559
- ${heartbeatWillRun ? 'bg-emerald-500/10 hover:bg-emerald-500/15 text-emerald-400' : 'bg-white/[0.04] hover:bg-white/[0.07] text-text-3'}`}
560
- title={heartbeatWillRun ? 'Toggle heartbeat' : !heartbeatEnabled ? 'Heartbeat disabled — click to enable' : 'Heartbeat enabled but paused (bounded loop mode, no explicit opt-in)'}
561
- >
562
- <span className={`w-1.5 h-1.5 rounded-full ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/40'}`} />
563
- <span className="text-[11px] font-600">
564
- HB {heartbeatWillRun ? 'On' : 'Off'}
565
- </span>
566
- {heartbeatEnabled && !loopIsOngoing && !heartbeatExplicitOptIn && (
567
- <span className="text-[10px] text-text-3/50">(bounded)</span>
568
- )}
569
- </button>
570
- <div className="relative" ref={hbDropdownRef}>
571
- <button
572
- onClick={() => setHbDropdownOpen((o) => !o)}
573
- disabled={heartbeatSaving}
574
- className="flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-text-3 transition-colors cursor-pointer border-none"
575
- title="Set heartbeat interval"
576
- >
577
- <span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
578
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/50">
579
- <polyline points="6 9 12 15 18 9" />
580
- </svg>
581
- </button>
582
- {hbDropdownOpen && (
583
- <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-[80px]">
584
- {[30, 60, 120, 300, 600, 1800, 3600].map((sec) => (
585
- <button
586
- key={sec}
587
- onClick={() => handleSelectHeartbeatInterval(sec)}
588
- className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
589
- ${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
590
- >
591
- {formatDuration(sec)}
592
- </button>
593
- ))}
594
- </div>
595
- )}
596
- </div>
597
- </>
794
+ {/* Context bar: tools, mission controls, links */}
795
+ {hasContextBar && (
796
+ <div className="flex items-center gap-1.5 px-3.5 pb-1.5 overflow-x-auto scrollbar-none">
797
+ {hasToolToggles && <ChatToolToggles session={session} />}
798
+ {hasToolToggles && (hasMemoryLink || isMainSession || linkedTask || resumeHandle || isOpenClawAgent || browserActive) && (
799
+ <div className="w-px h-4 bg-white/[0.05] shrink-0" />
598
800
  )}
599
801
  {isMainSession && (
600
802
  <>
601
803
  <button
602
804
  onClick={handleToggleMissionPause}
603
805
  disabled={mainLoopSaving}
604
- className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
605
- ${missionPaused ? 'bg-amber-500/12 hover:bg-amber-500/20 text-amber-300' : 'bg-emerald-500/10 hover:bg-emerald-500/15 text-emerald-400'}`}
606
- title={missionPaused ? 'Resume autonomous mission loop' : 'Pause autonomous mission loop'}
806
+ className={`flex items-center gap-1.5 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
807
+ ${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'}`}
808
+ title={missionPaused ? 'Resume mission' : 'Pause mission'}
607
809
  >
608
810
  <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
609
- <span className="text-[11px] font-600">
610
- Mission {missionPaused ? 'Paused' : 'Live'}
611
- </span>
811
+ {missionPaused ? 'Paused' : 'Live'}
612
812
  </button>
613
813
  <button
614
814
  onClick={handleToggleMissionMode}
615
815
  disabled={mainLoopSaving}
616
- className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
617
- ${missionMode === 'autonomous'
618
- ? 'bg-indigo-500/15 hover:bg-indigo-500/25 text-indigo-200'
619
- : 'bg-white/[0.04] hover:bg-white/[0.07] text-text-3'
620
- }`}
621
- title="Toggle mission autonomy mode"
816
+ className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
817
+ ${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'}`}
818
+ title="Toggle autonomy mode"
622
819
  >
623
- <span className="text-[11px] font-600">
624
- Mode {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
625
- </span>
820
+ {missionMode === 'autonomous' ? 'Auto' : 'Assist'}
626
821
  </button>
627
822
  <button
628
823
  onClick={handleNudgeMission}
629
824
  disabled={mainLoopSaving || missionPaused}
630
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#3B82F6]/10 hover:bg-[#3B82F6]/18 text-[#60A5FA] transition-colors cursor-pointer border-none disabled:opacity-60"
631
- title="Run one immediate main-loop mission tick"
825
+ 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"
826
+ title="Run one tick"
632
827
  >
633
- <span className="text-[11px] font-600">Nudge</span>
828
+ Nudge
634
829
  </button>
635
830
  <button
636
831
  onClick={handleSetMissionGoal}
637
832
  disabled={mainLoopSaving}
638
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-fuchsia-500/10 hover:bg-fuchsia-500/18 text-fuchsia-300 transition-colors cursor-pointer border-none"
639
- title="Set an explicit mission goal"
833
+ 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"
834
+ title="Set mission goal"
640
835
  >
641
- <span className="text-[11px] font-600">Set Goal</span>
836
+ Goal
642
837
  </button>
643
838
  {missionEventsCount > 0 && (
644
839
  <button
645
840
  onClick={handleClearMissionEvents}
646
841
  disabled={mainLoopSaving}
647
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-text-3 transition-colors cursor-pointer border-none"
648
- title="Clear pending mission events"
842
+ 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"
843
+ title="Clear pending events"
649
844
  >
650
- <span className="text-[11px] font-600">Events {missionEventsCount}</span>
845
+ Events {missionEventsCount}
651
846
  </button>
652
847
  )}
653
- <span className="text-[10px] text-text-3/50 uppercase tracking-wider">
654
- {`State ${missionStatus}${missionMomentum !== null ? ` · ${missionMomentum}` : ''}`}
848
+ <span className="text-[9px] text-text-3/40 uppercase tracking-wider shrink-0">
849
+ {missionStatus}{missionMomentum !== null ? ` · ${missionMomentum}` : ''}
655
850
  </span>
656
- {mainLoopError && (
657
- <span className="text-[10px] text-red-300/90 truncate max-w-[280px]" title={mainLoopError}>
658
- {mainLoopError}
659
- </span>
660
- )}
661
- {mainLoopNotice && (
662
- <span className="text-[10px] text-emerald-300/90 truncate max-w-[220px]" title={mainLoopNotice}>
663
- {mainLoopNotice}
664
- </span>
665
- )}
851
+ {mainLoopError && <span className="text-[9px] text-red-300/80 truncate max-w-[240px]" title={mainLoopError}>{mainLoopError}</span>}
852
+ {mainLoopNotice && <span className="text-[9px] text-emerald-300/80 truncate max-w-[200px]" title={mainLoopNotice}>{mainLoopNotice}</span>}
666
853
  </>
667
854
  )}
668
- {agent && session.tools?.includes('memory') && (
855
+ {hasMemoryLink && (
669
856
  <button
670
- onClick={() => {
671
- setMemoryAgentFilter(session.agentId!)
672
- setActiveView('memory')
673
- setSidebarOpen(true)
674
- }}
675
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-accent-soft/50 hover:bg-accent-soft transition-colors cursor-pointer"
857
+ onClick={() => { setMemoryAgentFilter(session.agentId!); setActiveView('memory'); setSidebarOpen(true) }}
858
+ 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"
676
859
  >
677
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright/60">
860
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
678
861
  <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" />
679
862
  </svg>
680
- <span className="text-[11px] font-600 text-accent-bright/60">
681
- {agent.name} Memories
682
- </span>
863
+ Memories
683
864
  </button>
684
865
  )}
685
866
  {isOpenClawAgent && openclawSessionKey && (
@@ -687,78 +868,66 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
687
868
  <button
688
869
  onClick={handleSyncHistory}
689
870
  disabled={syncingHistory}
690
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-indigo-500/10 hover:bg-indigo-500/15 transition-colors cursor-pointer border-none disabled:opacity-50"
691
- title="Sync chat history from OpenClaw gateway"
871
+ 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"
872
+ title="Sync from gateway"
692
873
  >
693
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-indigo-400">
694
- <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
695
- <path d="M3 3v5h5" />
696
- <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
697
- <path d="M16 16h5v5" />
874
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
875
+ <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" />
876
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" /><path d="M16 16h5v5" />
698
877
  </svg>
699
- <span className="text-[11px] font-600 text-indigo-400">
700
- {syncingHistory ? 'Syncing...' : 'Sync History'}
701
- </span>
878
+ {syncingHistory ? 'Syncing...' : 'Sync'}
702
879
  </button>
703
- {syncResult && (
704
- <span className="text-[10px] text-emerald-300/90">{syncResult}</span>
705
- )}
880
+ {syncResult && <span className="text-[9px] text-emerald-300/80 shrink-0">{syncResult}</span>}
706
881
  </>
707
882
  )}
708
883
  {linkedTask && (
709
884
  <button
710
885
  onClick={() => setActiveView('tasks')}
711
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#F59E0B]/10 hover:bg-[#F59E0B]/15 transition-colors cursor-pointer"
886
+ 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"
712
887
  >
713
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#F59E0B" strokeWidth="2.5" strokeLinecap="round">
714
- <path d="M9 11l3 3L22 4" />
715
- <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
888
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
889
+ <path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
716
890
  </svg>
717
- <span className="text-[11px] font-600 text-[#F59E0B] truncate max-w-[200px]">
718
- Task: {linkedTask.title}
719
- </span>
891
+ <span className="truncate max-w-[160px]">{linkedTask.title}</span>
720
892
  </button>
721
893
  )}
722
894
  {resumeHandle && (
723
- <button
724
- onClick={handleCopySessionId}
725
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] transition-colors cursor-pointer group"
726
- title="Copy resume handle/command to clipboard"
727
- >
728
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/50">
729
- <path d="M4 17l6 0l0 -6" />
730
- <path d="M20 7l-6 0l0 6" />
731
- <path d="M4 17l10 -10" />
732
- </svg>
733
- <span className="text-[11px] font-mono text-text-3/50 group-hover:text-text-3/70 truncate max-w-[220px]">
734
- {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
735
- </span>
736
- {!copied && (
737
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/60 shrink-0">
738
- <rect x="9" y="9" width="13" height="13" rx="2" />
739
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
895
+ <div className="flex items-center rounded-[7px] bg-white/[0.03] group/resume shrink-0">
896
+ <button
897
+ onClick={handleCopySessionId}
898
+ className="flex items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
899
+ title="Copy resume command"
900
+ >
901
+ <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">
902
+ <path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
740
903
  </svg>
741
- )}
742
- </button>
904
+ <span className="text-[10px] font-mono text-text-3/40 group-hover/resume:text-text-3/60 truncate max-w-[180px]">
905
+ {copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
906
+ </span>
907
+ </button>
908
+ <button
909
+ onClick={handleDismissResumeHandle}
910
+ className="px-1 py-1 rounded-r-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer opacity-0 group-hover/resume:opacity-100"
911
+ title="Dismiss"
912
+ >
913
+ <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">
914
+ <path d="M4 4l8 8M12 4l-8 8" />
915
+ </svg>
916
+ </button>
917
+ </div>
743
918
  )}
744
919
  {browserActive && (
745
920
  <button
746
921
  onClick={onStopBrowser}
747
- className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#3B82F6]/10 hover:bg-[#F43F5E]/15 transition-colors cursor-pointer group"
922
+ 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"
748
923
  title="Stop browser"
749
924
  >
750
- <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-[#3B82F6] group-hover:text-[#F43F5E]">
751
- <rect x="3" y="3" width="18" height="14" rx="2" />
752
- <path d="M3 9h18" />
753
- <circle cx="7" cy="6" r="0.5" fill="currentColor" />
754
- <circle cx="10" cy="6" r="0.5" fill="currentColor" />
925
+ <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">
926
+ <rect x="3" y="3" width="18" height="14" rx="2" /><path d="M3 9h18" />
755
927
  </svg>
756
- <span className="text-[11px] font-600 text-[#3B82F6] group-hover:text-[#F43F5E]">
757
- Browser Active
758
- </span>
759
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/60 group-hover:text-[#F43F5E] shrink-0">
760
- <line x1="18" y1="6" x2="6" y2="18" />
761
- <line x1="6" y1="6" x2="18" y2="18" />
928
+ <span className="text-accent-bright group-hover:text-red-400">Browser</span>
929
+ <svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/40 group-hover:text-red-400">
930
+ <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
762
931
  </svg>
763
932
  </button>
764
933
  )}