@swarmclawai/swarmclaw 0.4.5 → 0.5.0
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 +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CONNECTOR_PLATFORM_META,
|
|
14
14
|
getSessionConnector,
|
|
15
15
|
} from '@/components/shared/connector-platform-icon'
|
|
16
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
16
17
|
|
|
17
18
|
function shortPath(p: string): string {
|
|
18
19
|
return (p || '').replace(/^\/Users\/\w+/, '~')
|
|
@@ -52,9 +53,12 @@ interface Props {
|
|
|
52
53
|
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported }: Props) {
|
|
53
54
|
const ttsEnabled = useChatStore((s) => s.ttsEnabled)
|
|
54
55
|
const toggleTts = useChatStore((s) => s.toggleTts)
|
|
56
|
+
const soundEnabled = useChatStore((s) => s.soundEnabled)
|
|
57
|
+
const toggleSound = useChatStore((s) => s.toggleSound)
|
|
55
58
|
const debugOpen = useChatStore((s) => s.debugOpen)
|
|
56
59
|
const setDebugOpen = useChatStore((s) => s.setDebugOpen)
|
|
57
60
|
const lastUsage = useChatStore((s) => s.lastUsage)
|
|
61
|
+
const agentStatus = useChatStore((s) => s.agentStatus)
|
|
58
62
|
const agents = useAppStore((s) => s.agents)
|
|
59
63
|
const tasks = useAppStore((s) => s.tasks)
|
|
60
64
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
@@ -63,12 +67,15 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
63
67
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
64
68
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
65
69
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
70
|
+
const inspectorOpen = useAppStore((s) => s.inspectorOpen)
|
|
71
|
+
const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
|
|
66
72
|
const connectors = useAppStore((s) => s.connectors)
|
|
67
73
|
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
68
74
|
const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
|
|
69
75
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
70
76
|
const connector = getSessionConnector(session, connectors)
|
|
71
77
|
const connectorMeta = connector ? CONNECTOR_PLATFORM_META[connector.platform] : null
|
|
78
|
+
const connectorPresence = connector?.presence
|
|
72
79
|
const modelName = session.model || agent?.model || ''
|
|
73
80
|
const [copied, setCopied] = useState(false)
|
|
74
81
|
const [heartbeatSaving, setHeartbeatSaving] = useState(false)
|
|
@@ -77,6 +84,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
77
84
|
const [mainLoopSaving, setMainLoopSaving] = useState(false)
|
|
78
85
|
const [mainLoopError, setMainLoopError] = useState('')
|
|
79
86
|
const [mainLoopNotice, setMainLoopNotice] = useState('')
|
|
87
|
+
const [syncingHistory, setSyncingHistory] = useState(false)
|
|
88
|
+
const [syncResult, setSyncResult] = useState('')
|
|
80
89
|
|
|
81
90
|
// Find linked task for this session
|
|
82
91
|
const linkedTask = useMemo(() => {
|
|
@@ -135,7 +144,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
135
144
|
const total = (m[1] ? parseInt(m[1]) * 3600 : 0) + (m[2] ? parseInt(m[2]) * 60 : 0) + (m[3] ? parseInt(m[3]) : 0)
|
|
136
145
|
return Math.max(0, Math.min(86400, total))
|
|
137
146
|
}
|
|
138
|
-
const resolveFrom = (obj:
|
|
147
|
+
const resolveFrom = (obj: { heartbeatInterval?: string | number | null; heartbeatIntervalSec?: number | null }): number | null => {
|
|
139
148
|
const dur = parseDur(obj.heartbeatInterval)
|
|
140
149
|
if (dur !== null) return dur
|
|
141
150
|
const sec = parseDur(obj.heartbeatIntervalSec)
|
|
@@ -143,20 +152,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
143
152
|
return null
|
|
144
153
|
}
|
|
145
154
|
// Global defaults
|
|
146
|
-
let sec = resolveFrom(appSettings
|
|
155
|
+
let sec = resolveFrom(appSettings) ?? 1800
|
|
147
156
|
let enabled = sec > 0
|
|
148
157
|
let explicitOptIn = false
|
|
149
158
|
// Agent layer
|
|
150
159
|
if (agent) {
|
|
151
160
|
if (agent.heartbeatEnabled === false) enabled = false
|
|
152
161
|
if (agent.heartbeatEnabled === true) { enabled = true; explicitOptIn = true }
|
|
153
|
-
sec = resolveFrom(agent
|
|
162
|
+
sec = resolveFrom(agent) ?? sec
|
|
154
163
|
}
|
|
155
164
|
// Session layer — only applies for non-agent chats (agent chats save directly to agent)
|
|
156
165
|
if (!agent) {
|
|
157
166
|
if (session.heartbeatEnabled === false) enabled = false
|
|
158
167
|
if (session.heartbeatEnabled === true) { enabled = true; explicitOptIn = true }
|
|
159
|
-
sec = resolveFrom(session
|
|
168
|
+
sec = resolveFrom(session) ?? sec
|
|
160
169
|
}
|
|
161
170
|
return {
|
|
162
171
|
heartbeatEnabled: enabled && sec > 0,
|
|
@@ -268,6 +277,43 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
268
277
|
void postMainLoopAction('clear_events')
|
|
269
278
|
}
|
|
270
279
|
|
|
280
|
+
const isOpenClawAgent = agent?.provider === 'openclaw'
|
|
281
|
+
// Derive OpenClaw session key: agent sessions use "agent:<name>:main" convention
|
|
282
|
+
const openclawSessionKey = isOpenClawAgent && agent
|
|
283
|
+
? `agent:${agent.name.toLowerCase().replace(/\s+/g, '-')}:main`
|
|
284
|
+
: null
|
|
285
|
+
|
|
286
|
+
const handleSyncHistory = async () => {
|
|
287
|
+
if (!openclawSessionKey || syncingHistory) return
|
|
288
|
+
setSyncingHistory(true)
|
|
289
|
+
setSyncResult('')
|
|
290
|
+
try {
|
|
291
|
+
const preview = await api<{ sessionKey: string; epoch: number; messages: Array<{ role: string; content: string; ts: number }> }>(
|
|
292
|
+
'GET', `/openclaw/history?sessionKey=${encodeURIComponent(openclawSessionKey)}`,
|
|
293
|
+
)
|
|
294
|
+
if (!preview?.messages?.length) {
|
|
295
|
+
setSyncResult('No new messages found.')
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
const result = await api<{ ok: boolean; merged: number }>(
|
|
299
|
+
'POST', '/openclaw/history',
|
|
300
|
+
{ sessionKey: openclawSessionKey, epoch: preview.epoch, localSessionId: session.id },
|
|
301
|
+
)
|
|
302
|
+
setSyncResult(result.merged > 0 ? `Synced ${result.merged} message${result.merged !== 1 ? 's' : ''}.` : 'Already up to date.')
|
|
303
|
+
if (result.merged > 0) await loadSessions()
|
|
304
|
+
} catch (err: unknown) {
|
|
305
|
+
setSyncResult(err instanceof Error ? err.message : 'Sync failed.')
|
|
306
|
+
} finally {
|
|
307
|
+
setSyncingHistory(false)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
if (!syncResult) return
|
|
313
|
+
const timer = setTimeout(() => setSyncResult(''), 3000)
|
|
314
|
+
return () => clearTimeout(timer)
|
|
315
|
+
}, [syncResult])
|
|
316
|
+
|
|
271
317
|
useEffect(() => {
|
|
272
318
|
if (!hbDropdownOpen) return
|
|
273
319
|
const handler = (e: MouseEvent) => {
|
|
@@ -307,6 +353,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
307
353
|
)}
|
|
308
354
|
<div className="flex-1 min-w-0">
|
|
309
355
|
<div className="flex items-center gap-2.5">
|
|
356
|
+
{agent && <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />}
|
|
310
357
|
<span className="font-display text-[16px] font-600 block truncate tracking-[-0.02em]">{
|
|
311
358
|
session.name === '__main__' ? 'Main Chat'
|
|
312
359
|
: session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
|
|
@@ -326,6 +373,27 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
326
373
|
{connectorMeta.label}
|
|
327
374
|
</span>
|
|
328
375
|
)}
|
|
376
|
+
{connector && connectorPresence && (() => {
|
|
377
|
+
const lastAt = connectorPresence.lastMessageAt
|
|
378
|
+
if (!lastAt) return (
|
|
379
|
+
<span className="shrink-0 inline-flex items-center gap-1 text-[10px] text-text-3/50">
|
|
380
|
+
<span className="w-1.5 h-1.5 rounded-full bg-text-3/40" />
|
|
381
|
+
Inactive
|
|
382
|
+
</span>
|
|
383
|
+
)
|
|
384
|
+
const ago = Date.now() - lastAt
|
|
385
|
+
const isActive = ago < 5 * 60_000
|
|
386
|
+
const isRecent = ago < 30 * 60_000
|
|
387
|
+
const label = isActive ? 'Active' : isRecent ? `${Math.floor(ago / 60_000)}m ago` : 'Inactive'
|
|
388
|
+
const dotColor = isActive ? 'bg-emerald-400' : isRecent ? 'bg-amber-400' : 'bg-text-3/40'
|
|
389
|
+
const textColor = isActive ? 'text-emerald-400' : isRecent ? 'text-amber-300' : 'text-text-3/50'
|
|
390
|
+
return (
|
|
391
|
+
<span className={`shrink-0 inline-flex items-center gap-1 text-[10px] ${textColor}`}>
|
|
392
|
+
<span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
|
|
393
|
+
{label}
|
|
394
|
+
</span>
|
|
395
|
+
)
|
|
396
|
+
})()}
|
|
329
397
|
{session.provider && session.provider !== 'claude-cli' && (
|
|
330
398
|
<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">
|
|
331
399
|
{providerLabel}
|
|
@@ -351,6 +419,21 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
351
419
|
<>
|
|
352
420
|
<span className="text-[11px] text-text-3/60">·</span>
|
|
353
421
|
<span className="text-[11px] text-text-3/50 font-mono truncate shrink-0">{modelName}</span>
|
|
422
|
+
{session.conversationTone && session.conversationTone !== 'neutral' && (() => {
|
|
423
|
+
const toneColors: Record<string, string> = {
|
|
424
|
+
formal: 'bg-[#3B82F6]',
|
|
425
|
+
casual: 'bg-emerald-400',
|
|
426
|
+
empathetic: 'bg-purple-400',
|
|
427
|
+
technical: 'bg-[#F59E0B]',
|
|
428
|
+
}
|
|
429
|
+
const color = toneColors[session.conversationTone] || ''
|
|
430
|
+
return color ? (
|
|
431
|
+
<span
|
|
432
|
+
className={`w-2 h-2 rounded-full shrink-0 ${color}`}
|
|
433
|
+
title={`Tone: ${session.conversationTone}`}
|
|
434
|
+
/>
|
|
435
|
+
) : null
|
|
436
|
+
})()}
|
|
354
437
|
</>
|
|
355
438
|
)}
|
|
356
439
|
{lastUsage && !streaming && (
|
|
@@ -360,6 +443,50 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
360
443
|
</>
|
|
361
444
|
)}
|
|
362
445
|
</div>
|
|
446
|
+
{(() => {
|
|
447
|
+
const liveStatus = agentStatus || (missionState.status ? {
|
|
448
|
+
goal: missionState.goal ?? undefined,
|
|
449
|
+
status: missionState.status ?? undefined,
|
|
450
|
+
summary: missionState.summary ?? undefined,
|
|
451
|
+
nextAction: missionState.nextAction ?? undefined,
|
|
452
|
+
} : null)
|
|
453
|
+
if (!liveStatus) return null
|
|
454
|
+
const statusColors: Record<string, string> = {
|
|
455
|
+
idle: 'bg-text-3/40',
|
|
456
|
+
progress: 'bg-[#3B82F6]',
|
|
457
|
+
blocked: 'bg-amber-400',
|
|
458
|
+
ok: 'bg-emerald-400',
|
|
459
|
+
}
|
|
460
|
+
const dotColor = statusColors[liveStatus.status || ''] || 'bg-text-3/40'
|
|
461
|
+
return (
|
|
462
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
463
|
+
{liveStatus.goal && (
|
|
464
|
+
<span className="text-[10px] text-text-3/60 font-mono truncate max-w-[240px]" title={liveStatus.goal}>
|
|
465
|
+
{liveStatus.goal}
|
|
466
|
+
</span>
|
|
467
|
+
)}
|
|
468
|
+
{liveStatus.status && (
|
|
469
|
+
<span className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-wider ${
|
|
470
|
+
liveStatus.status === 'blocked' ? 'bg-amber-400/15 text-amber-300'
|
|
471
|
+
: liveStatus.status === 'ok' ? 'bg-emerald-400/15 text-emerald-400'
|
|
472
|
+
: liveStatus.status === 'progress' ? 'bg-[#3B82F6]/15 text-[#60A5FA]'
|
|
473
|
+
: 'bg-white/[0.04] text-text-3/60'
|
|
474
|
+
}`}>
|
|
475
|
+
<span className={`w-1.5 h-1.5 rounded-full ${dotColor}`} />
|
|
476
|
+
{liveStatus.status}
|
|
477
|
+
</span>
|
|
478
|
+
)}
|
|
479
|
+
{liveStatus.nextAction && (
|
|
480
|
+
<>
|
|
481
|
+
<span className="text-[10px] text-text-3/40">→</span>
|
|
482
|
+
<span className="text-[10px] text-text-3/50 font-mono truncate max-w-[200px]" title={liveStatus.nextAction}>
|
|
483
|
+
{liveStatus.nextAction}
|
|
484
|
+
</span>
|
|
485
|
+
</>
|
|
486
|
+
)}
|
|
487
|
+
</div>
|
|
488
|
+
)
|
|
489
|
+
})()}
|
|
363
490
|
</div>
|
|
364
491
|
<div className="flex gap-1.5">
|
|
365
492
|
{streaming && (
|
|
@@ -369,6 +496,14 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
369
496
|
</svg>
|
|
370
497
|
</IconButton>
|
|
371
498
|
)}
|
|
499
|
+
{agent && (
|
|
500
|
+
<IconButton onClick={() => setInspectorOpen(!inspectorOpen)} active={inspectorOpen} aria-label="Toggle inspector panel">
|
|
501
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
502
|
+
<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" />
|
|
503
|
+
<circle cx="12" cy="12" r="3" />
|
|
504
|
+
</svg>
|
|
505
|
+
</IconButton>
|
|
506
|
+
)}
|
|
372
507
|
<IconButton onClick={() => setDebugOpen(!debugOpen)} active={debugOpen} aria-label="Toggle debug panel">
|
|
373
508
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
374
509
|
<path d="M12 20V10" />
|
|
@@ -376,6 +511,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
376
511
|
<path d="M6 20v-4" />
|
|
377
512
|
</svg>
|
|
378
513
|
</IconButton>
|
|
514
|
+
<IconButton onClick={toggleSound} active={soundEnabled} aria-label="Toggle sound notifications">
|
|
515
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
516
|
+
<path d="M18 8A6 6 0 0 1 18 16" />
|
|
517
|
+
<path d="M13 2L8 7H4a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h4l5 5V2z" />
|
|
518
|
+
</svg>
|
|
519
|
+
</IconButton>
|
|
379
520
|
<IconButton onClick={toggleTts} active={ttsEnabled} aria-label="Toggle text-to-speech">
|
|
380
521
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
381
522
|
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
|
|
@@ -539,6 +680,29 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
539
680
|
</span>
|
|
540
681
|
</button>
|
|
541
682
|
)}
|
|
683
|
+
{isOpenClawAgent && openclawSessionKey && (
|
|
684
|
+
<>
|
|
685
|
+
<button
|
|
686
|
+
onClick={handleSyncHistory}
|
|
687
|
+
disabled={syncingHistory}
|
|
688
|
+
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-indigo-500/10 hover:bg-indigo-500/15 transition-colors cursor-pointer border-none disabled:opacity-50"
|
|
689
|
+
title="Sync chat history from OpenClaw gateway"
|
|
690
|
+
>
|
|
691
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-indigo-400">
|
|
692
|
+
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
693
|
+
<path d="M3 3v5h5" />
|
|
694
|
+
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
|
|
695
|
+
<path d="M16 16h5v5" />
|
|
696
|
+
</svg>
|
|
697
|
+
<span className="text-[11px] font-600 text-indigo-400">
|
|
698
|
+
{syncingHistory ? 'Syncing...' : 'Sync History'}
|
|
699
|
+
</span>
|
|
700
|
+
</button>
|
|
701
|
+
{syncResult && (
|
|
702
|
+
<span className="text-[10px] text-emerald-300/90">{syncResult}</span>
|
|
703
|
+
)}
|
|
704
|
+
</>
|
|
705
|
+
)}
|
|
542
706
|
{linkedTask && (
|
|
543
707
|
<button
|
|
544
708
|
onClick={() => setActiveView('tasks')}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from 'react'
|
|
4
|
+
import { CodeBlock } from './code-block'
|
|
5
|
+
|
|
6
|
+
interface PreviewContent {
|
|
7
|
+
type: 'browser' | 'image' | 'code' | 'html'
|
|
8
|
+
url?: string
|
|
9
|
+
content?: string
|
|
10
|
+
title?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
content: PreviewContent
|
|
15
|
+
onClose: () => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ChatPreviewPanel({ content, onClose }: Props) {
|
|
19
|
+
const [width, setWidth] = useState(400)
|
|
20
|
+
const dragging = useRef(false)
|
|
21
|
+
const startX = useRef(0)
|
|
22
|
+
const startWidth = useRef(400)
|
|
23
|
+
|
|
24
|
+
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
|
25
|
+
e.preventDefault()
|
|
26
|
+
dragging.current = true
|
|
27
|
+
startX.current = e.clientX
|
|
28
|
+
startWidth.current = width
|
|
29
|
+
|
|
30
|
+
const handleMouseMove = (ev: MouseEvent) => {
|
|
31
|
+
if (!dragging.current) return
|
|
32
|
+
const diff = startX.current - ev.clientX
|
|
33
|
+
const next = Math.max(300, Math.min(window.innerWidth * 0.5, startWidth.current + diff))
|
|
34
|
+
setWidth(next)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const handleMouseUp = () => {
|
|
38
|
+
dragging.current = false
|
|
39
|
+
document.removeEventListener('mousemove', handleMouseMove)
|
|
40
|
+
document.removeEventListener('mouseup', handleMouseUp)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
document.addEventListener('mousemove', handleMouseMove)
|
|
44
|
+
document.addEventListener('mouseup', handleMouseUp)
|
|
45
|
+
}, [width])
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className="flex flex-col border-l border-white/[0.06] bg-bg shrink-0"
|
|
50
|
+
style={{ width, minWidth: 300, maxWidth: '50%', animation: 'fade-in 0.25s ease' }}
|
|
51
|
+
>
|
|
52
|
+
{/* Resize handle */}
|
|
53
|
+
<div
|
|
54
|
+
className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-accent-bright/20 transition-colors z-10"
|
|
55
|
+
style={{ position: 'relative', width: 4, minWidth: 4 }}
|
|
56
|
+
onMouseDown={handleMouseDown}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
{/* Header */}
|
|
60
|
+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-white/[0.06] shrink-0">
|
|
61
|
+
<span className="text-[12px] font-600 text-text-2 truncate flex-1">
|
|
62
|
+
{content.title || 'Preview'}
|
|
63
|
+
</span>
|
|
64
|
+
<button
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
className="p-1 rounded-[6px] text-text-3 hover:text-text-2 hover:bg-white/[0.04] cursor-pointer border-none bg-transparent transition-colors"
|
|
67
|
+
aria-label="Close preview"
|
|
68
|
+
>
|
|
69
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
70
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
71
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Content */}
|
|
77
|
+
<div className="flex-1 overflow-auto min-h-0">
|
|
78
|
+
{content.type === 'browser' && content.url && (
|
|
79
|
+
<iframe
|
|
80
|
+
src={content.url}
|
|
81
|
+
className="w-full h-full border-none"
|
|
82
|
+
title={content.title || 'Browser Preview'}
|
|
83
|
+
sandbox="allow-scripts allow-same-origin"
|
|
84
|
+
/>
|
|
85
|
+
)}
|
|
86
|
+
{content.type === 'html' && content.content && (
|
|
87
|
+
<iframe
|
|
88
|
+
srcDoc={content.content}
|
|
89
|
+
className="w-full h-full border-none"
|
|
90
|
+
title={content.title || 'HTML Preview'}
|
|
91
|
+
sandbox="allow-scripts"
|
|
92
|
+
/>
|
|
93
|
+
)}
|
|
94
|
+
{content.type === 'image' && content.url && (
|
|
95
|
+
<div className="p-4 flex items-center justify-center h-full">
|
|
96
|
+
<img
|
|
97
|
+
src={content.url}
|
|
98
|
+
alt={content.title || 'Preview'}
|
|
99
|
+
className="max-w-full max-h-full rounded-[8px] object-contain"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
{content.type === 'code' && content.content && (
|
|
104
|
+
<div className="p-2">
|
|
105
|
+
<CodeBlock className={`language-${content.title?.split('.').pop() || 'text'}`}>
|
|
106
|
+
{content.content}
|
|
107
|
+
</CodeBlock>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
|
|
4
|
+
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
approval: PendingExecApproval
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ExecApprovalCard({ approval }: Props) {
|
|
11
|
+
const resolveApproval = useApprovalStore((s) => s.resolveApproval)
|
|
12
|
+
|
|
13
|
+
const handleResolve = (decision: ExecApprovalDecision) => {
|
|
14
|
+
resolveApproval(approval.id, decision)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const expired = approval.expiresAtMs < Date.now()
|
|
18
|
+
const disabled = !!approval.resolving || expired
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="my-2 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] p-3.5">
|
|
22
|
+
<div className="flex items-center gap-2 mb-2">
|
|
23
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400 shrink-0">
|
|
24
|
+
<path d="M12 9v2m0 4h.01" />
|
|
25
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
26
|
+
</svg>
|
|
27
|
+
<span className="text-[12px] font-600 text-amber-400">Execution Approval Required</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
{approval.ask && (
|
|
31
|
+
<p className="text-[13px] text-text-2 mb-2">{approval.ask}</p>
|
|
32
|
+
)}
|
|
33
|
+
|
|
34
|
+
<div className="rounded-[8px] bg-black/20 px-3 py-2 mb-2 overflow-x-auto">
|
|
35
|
+
<code className="text-[12px] text-text font-mono whitespace-pre-wrap break-all">
|
|
36
|
+
{approval.command}
|
|
37
|
+
</code>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div className="flex flex-wrap gap-x-4 gap-y-1 text-[11px] text-text-3/60 mb-3">
|
|
41
|
+
{approval.cwd && <span>cwd: {approval.cwd}</span>}
|
|
42
|
+
{approval.host && <span>host: {approval.host}</span>}
|
|
43
|
+
{approval.security && (
|
|
44
|
+
<span className={approval.security === 'high' ? 'text-red-400' : ''}>
|
|
45
|
+
security: {approval.security}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{approval.error && (
|
|
51
|
+
<p className="text-[12px] text-red-400 mb-2">{approval.error}</p>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{expired ? (
|
|
55
|
+
<p className="text-[12px] text-text-3/50 italic">Approval expired</p>
|
|
56
|
+
) : (
|
|
57
|
+
<div className="flex items-center gap-2">
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => handleResolve('allow-once')}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-emerald-500/10 text-[12px] font-600
|
|
62
|
+
text-emerald-400 cursor-pointer hover:bg-emerald-500/20 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
63
|
+
style={{ fontFamily: 'inherit' }}
|
|
64
|
+
>
|
|
65
|
+
{approval.resolving ? '...' : 'Allow Once'}
|
|
66
|
+
</button>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => handleResolve('allow-always')}
|
|
69
|
+
disabled={disabled}
|
|
70
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
71
|
+
text-text-3 cursor-pointer hover:bg-white/[0.04] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
72
|
+
style={{ fontFamily: 'inherit' }}
|
|
73
|
+
>
|
|
74
|
+
Always Allow
|
|
75
|
+
</button>
|
|
76
|
+
<button
|
|
77
|
+
onClick={() => handleResolve('deny')}
|
|
78
|
+
disabled={disabled}
|
|
79
|
+
className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[12px] font-600
|
|
80
|
+
text-red-400 cursor-pointer hover:bg-red-400/10 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
81
|
+
style={{ fontFamily: 'inherit' }}
|
|
82
|
+
>
|
|
83
|
+
Deny
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
}
|