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