@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.
- package/README.md +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- 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
|
|
347
|
-
|
|
348
|
-
|
|
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="
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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="
|
|
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}
|
|
370
|
-
borderColor: `${connectorMeta.color}
|
|
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={
|
|
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/
|
|
382
|
-
<span className="w-1.5 h-1.5 rounded-full bg-text-3/
|
|
383
|
-
|
|
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` : '
|
|
390
|
-
const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/
|
|
391
|
-
const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/
|
|
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="
|
|
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
|
-
|
|
419
|
-
|
|
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
|
-
<
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
618
|
+
</div>
|
|
619
|
+
)}
|
|
620
|
+
</div>
|
|
440
621
|
)}
|
|
441
622
|
{lastUsage && !streaming && (
|
|
442
623
|
<>
|
|
443
|
-
<span className="text-[
|
|
624
|
+
<span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
|
|
444
625
|
<UsageBadge {...lastUsage} />
|
|
445
626
|
</>
|
|
446
627
|
)}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
<
|
|
497
|
-
<
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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={
|
|
510
|
-
<svg width="
|
|
511
|
-
<path d="
|
|
512
|
-
<path d="
|
|
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={
|
|
517
|
-
<svg width="
|
|
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
|
|
530
|
-
<svg width="
|
|
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
|
-
|
|
538
|
-
<
|
|
539
|
-
<
|
|
540
|
-
|
|
541
|
-
|
|
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
|
-
{/*
|
|
548
|
-
{
|
|
549
|
-
<div className="flex items-center gap-
|
|
550
|
-
{
|
|
551
|
-
|
|
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
|
|
605
|
-
${missionPaused ? 'bg-amber-500/
|
|
606
|
-
title={missionPaused ? 'Resume
|
|
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
|
-
|
|
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
|
|
617
|
-
${missionMode === 'autonomous'
|
|
618
|
-
|
|
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
|
-
|
|
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="
|
|
631
|
-
title="Run one
|
|
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
|
-
|
|
828
|
+
Nudge
|
|
634
829
|
</button>
|
|
635
830
|
<button
|
|
636
831
|
onClick={handleSetMissionGoal}
|
|
637
832
|
disabled={mainLoopSaving}
|
|
638
|
-
className="
|
|
639
|
-
title="Set
|
|
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
|
-
|
|
836
|
+
Goal
|
|
642
837
|
</button>
|
|
643
838
|
{missionEventsCount > 0 && (
|
|
644
839
|
<button
|
|
645
840
|
onClick={handleClearMissionEvents}
|
|
646
841
|
disabled={mainLoopSaving}
|
|
647
|
-
className="
|
|
648
|
-
title="Clear pending
|
|
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
|
-
|
|
845
|
+
Events {missionEventsCount}
|
|
651
846
|
</button>
|
|
652
847
|
)}
|
|
653
|
-
<span className="text-[
|
|
654
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
855
|
+
{hasMemoryLink && (
|
|
669
856
|
<button
|
|
670
|
-
onClick={() => {
|
|
671
|
-
|
|
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="
|
|
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
|
-
|
|
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
|
|
691
|
-
title="Sync
|
|
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="
|
|
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
|
|
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
|
-
|
|
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
|
|
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="
|
|
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="
|
|
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
|
-
<
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
<
|
|
730
|
-
|
|
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
|
-
|
|
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
|
|
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="
|
|
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-
|
|
757
|
-
|
|
758
|
-
|
|
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
|
)}
|