@swarmclawai/swarmclaw 0.7.1 → 0.7.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 +85 -139
- package/package.json +1 -1
- package/src/app/api/agents/[id]/thread/route.ts +1 -2
- package/src/app/api/agents/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/main-loop/route.ts +2 -2
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +4 -52
- package/src/app/api/{sessions → chats}/route.ts +5 -7
- package/src/app/api/plugins/route.ts +3 -0
- package/src/app/api/plugins/settings/route.ts +35 -0
- package/src/app/api/usage/route.ts +30 -0
- package/src/cli/index.js +35 -33
- package/src/cli/index.ts +40 -39
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-chat-list.tsx +3 -3
- package/src/components/agents/agent-list.tsx +8 -13
- package/src/components/agents/agent-sheet.tsx +2 -2
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +2 -2
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +10 -14
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +3 -3
- package/src/components/chat/chat-header.tsx +156 -73
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +4 -5
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +2 -2
- package/src/components/{sessions/new-session-sheet.tsx → chat/new-chat-sheet.tsx} +6 -6
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/connectors/connector-sheet.tsx +1 -1
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/layout/app-layout.tsx +23 -2
- package/src/components/plugins/plugin-list.tsx +475 -254
- package/src/components/plugins/plugin-sheet.tsx +124 -10
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/command-palette.tsx +0 -1
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/settings-page.tsx +1 -12
- package/src/components/usage/metrics-dashboard.tsx +73 -0
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/approvals.ts +4 -4
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution.ts +36 -105
- package/src/lib/server/chatroom-helpers.ts +3 -3
- package/src/lib/server/connectors/manager.ts +4 -4
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +2 -2
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/main-agent-loop.ts +25 -160
- package/src/lib/server/main-session.ts +6 -13
- package/src/lib/server/orchestrator-lg.ts +3 -3
- package/src/lib/server/orchestrator.ts +5 -5
- package/src/lib/server/plugins.ts +112 -4
- package/src/lib/server/provider-health.ts +5 -3
- package/src/lib/server/queue.ts +12 -10
- package/src/lib/server/session-run-manager.test.ts +9 -6
- package/src/lib/server/session-run-manager.ts +1 -3
- package/src/lib/server/session-tools/calendar.ts +376 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +5 -2
- package/src/lib/server/session-tools/context.ts +7 -3
- package/src/lib/server/session-tools/crud.ts +14 -6
- package/src/lib/server/session-tools/delegate.ts +95 -8
- package/src/lib/server/session-tools/discovery.ts +2 -2
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +322 -0
- package/src/lib/server/session-tools/file.ts +5 -2
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/image-gen.ts +382 -0
- package/src/lib/server/session-tools/index.ts +74 -49
- package/src/lib/server/session-tools/memory.ts +139 -2
- package/src/lib/server/session-tools/monitor.ts +1 -1
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform.ts +6 -3
- package/src/lib/server/session-tools/plugin-creator.ts +3 -3
- package/src/lib/server/session-tools/replicate.ts +303 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +4 -2
- package/src/lib/server/session-tools/session-info.ts +7 -4
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +2 -2
- package/src/lib/server/session-tools/wallet.ts +29 -2
- package/src/lib/server/session-tools/web.ts +44 -5
- package/src/lib/server/storage.ts +29 -9
- package/src/lib/server/stream-agent-chat.ts +72 -249
- package/src/lib/server/tool-aliases.ts +26 -15
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +32 -27
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.ts +3 -1
- package/src/stores/use-app-store.ts +5 -5
- package/src/stores/use-chat-store.ts +7 -7
- package/src/types/index.ts +65 -3
- /package/src/app/api/{sessions → chats}/[id]/browser/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/chat/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/messages/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/stop/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useMemo, useRef, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useState, useMemo, useRef, useCallback, type ReactNode } from 'react'
|
|
4
4
|
import type { Session } from '@/types'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -14,11 +14,24 @@ import {
|
|
|
14
14
|
} from '@/components/shared/connector-platform-icon'
|
|
15
15
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
16
16
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
17
|
+
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
17
18
|
import { toast } from 'sonner'
|
|
18
19
|
import type { ProviderType } from '@/types'
|
|
19
20
|
import { copyTextToClipboard } from '@/lib/clipboard'
|
|
20
21
|
import { useWs } from '@/hooks/use-ws'
|
|
21
22
|
|
|
23
|
+
function Tip({ label, children, side = 'bottom' }: { label: string; children: ReactNode; side?: 'top' | 'bottom' | 'left' | 'right' }) {
|
|
24
|
+
return (
|
|
25
|
+
<Tooltip>
|
|
26
|
+
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
|
27
|
+
<TooltipContent side={side} sideOffset={6}
|
|
28
|
+
className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[8px] px-2.5 py-1.5 text-[11px] z-[100]">
|
|
29
|
+
{label}
|
|
30
|
+
</TooltipContent>
|
|
31
|
+
</Tooltip>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
function shortPath(p: string): string {
|
|
23
36
|
return (p || '').replace(/^\/Users\/\w+/, '~')
|
|
24
37
|
}
|
|
@@ -109,6 +122,9 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
109
122
|
const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
|
|
110
123
|
const [walletBalance, setWalletBalance] = useState<number | null>(null)
|
|
111
124
|
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)
|
|
112
128
|
|
|
113
129
|
useEffect(() => {
|
|
114
130
|
api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
|
|
@@ -197,17 +213,17 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
197
213
|
const handleDismissResumeHandle = async (e: React.MouseEvent) => {
|
|
198
214
|
e.stopPropagation()
|
|
199
215
|
try {
|
|
200
|
-
await api('PUT', `/
|
|
216
|
+
await api('PUT', `/chats/${session.id}`, {
|
|
201
217
|
claudeSessionId: null,
|
|
202
218
|
codexThreadId: null,
|
|
203
219
|
opencodeSessionId: null,
|
|
204
|
-
delegateResumeIds: { claudeCode: null, codex: null, opencode: null },
|
|
220
|
+
delegateResumeIds: { claudeCode: null, codex: null, opencode: null, gemini: null },
|
|
205
221
|
})
|
|
206
222
|
await loadSessions()
|
|
207
223
|
} catch { /* best-effort */ }
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
const heartbeatSupported = (session.
|
|
226
|
+
const heartbeatSupported = (session.plugins?.length ?? 0) > 0
|
|
211
227
|
const loopIsOngoing = appSettings.loopMode === 'ongoing'
|
|
212
228
|
const { heartbeatEnabled, heartbeatIntervalSec, heartbeatExplicitOptIn } = useMemo(() => {
|
|
213
229
|
// Resolve through the same cascade as the backend: settings → agent → session
|
|
@@ -254,7 +270,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
254
270
|
}
|
|
255
271
|
}, [appSettings, agent, session])
|
|
256
272
|
const heartbeatWillRun = heartbeatEnabled && (loopIsOngoing || heartbeatExplicitOptIn)
|
|
257
|
-
const
|
|
273
|
+
const hasMainLoop = session.id.startsWith('agent-thread-') || session.sessionType === 'orchestrated'
|
|
258
274
|
const missionState = session.mainLoopState || {}
|
|
259
275
|
const missionPaused = missionState.paused === true
|
|
260
276
|
const missionMode = missionState.autonomyMode === 'assist' ? 'assist' : 'autonomous'
|
|
@@ -270,10 +286,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
270
286
|
if (session.agentId) {
|
|
271
287
|
await api('PUT', `/agents/${session.agentId}`, { heartbeatEnabled: next })
|
|
272
288
|
// Clear any stale session-level override so the agent value wins
|
|
273
|
-
await api('PUT', `/
|
|
289
|
+
await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: null })
|
|
274
290
|
await Promise.all([loadAgents(), loadSessions()])
|
|
275
291
|
} else {
|
|
276
|
-
await api('PUT', `/
|
|
292
|
+
await api('PUT', `/chats/${session.id}`, { heartbeatEnabled: next })
|
|
277
293
|
await loadSessions()
|
|
278
294
|
}
|
|
279
295
|
toast.success(`Heartbeat ${next ? 'enabled' : 'disabled'}`)
|
|
@@ -295,10 +311,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
295
311
|
heartbeatEnabled: true,
|
|
296
312
|
})
|
|
297
313
|
// Clear stale session-level overrides
|
|
298
|
-
await api('PUT', `/
|
|
314
|
+
await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
|
|
299
315
|
await Promise.all([loadAgents(), loadSessions()])
|
|
300
316
|
} else {
|
|
301
|
-
await api('PUT', `/
|
|
317
|
+
await api('PUT', `/chats/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
|
|
302
318
|
await loadSessions()
|
|
303
319
|
}
|
|
304
320
|
} finally {
|
|
@@ -307,10 +323,10 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
307
323
|
}
|
|
308
324
|
|
|
309
325
|
const postMainLoopAction = async (action: string, extra?: Record<string, unknown>) => {
|
|
310
|
-
if (!
|
|
326
|
+
if (!hasMainLoop || mainLoopSaving) return
|
|
311
327
|
setMainLoopSaving(true)
|
|
312
328
|
try {
|
|
313
|
-
const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/
|
|
329
|
+
const result = await api<{ runId?: string; deduped?: boolean }>('POST', `/chats/${session.id}/main-loop`, {
|
|
314
330
|
action,
|
|
315
331
|
...(extra || {}),
|
|
316
332
|
})
|
|
@@ -344,17 +360,22 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
344
360
|
void postMainLoopAction('nudge')
|
|
345
361
|
}
|
|
346
362
|
|
|
347
|
-
const
|
|
348
|
-
if (!
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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)
|
|
352
373
|
if (!goal) return
|
|
353
374
|
void postMainLoopAction('set_goal', { goal })
|
|
354
375
|
}
|
|
355
376
|
|
|
356
377
|
const handleClearMissionEvents = () => {
|
|
357
|
-
if (!
|
|
378
|
+
if (!hasMainLoop || missionEventsCount <= 0) return
|
|
358
379
|
void postMainLoopAction('clear_events')
|
|
359
380
|
}
|
|
360
381
|
|
|
@@ -473,7 +494,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
473
494
|
const handleModelSwitch = async (nextProvider: ProviderType, nextModel: string) => {
|
|
474
495
|
setModelSwitcherOpen(false)
|
|
475
496
|
try {
|
|
476
|
-
await api('PUT', `/
|
|
497
|
+
await api('PUT', `/chats/${session.id}`, { provider: nextProvider, model: nextModel })
|
|
477
498
|
await loadSessions()
|
|
478
499
|
} catch (err: unknown) {
|
|
479
500
|
toast.error(err instanceof Error ? err.message : 'Failed to switch model')
|
|
@@ -502,12 +523,13 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
502
523
|
}, [mainLoopNotice])
|
|
503
524
|
|
|
504
525
|
// Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
|
|
505
|
-
const hasToolToggles = ((agent?.
|
|
506
|
-
const hasMemoryLink = !!(agent && session.
|
|
526
|
+
const hasToolToggles = ((agent?.plugins?.length ?? 0) > 0) || ((session.plugins?.length ?? 0) > 0)
|
|
527
|
+
const hasMemoryLink = !!(agent && session.plugins?.includes('memory'))
|
|
507
528
|
const hasSourceFilter = !!hasMultipleSources
|
|
508
|
-
const hasContextBar = !!(
|
|
529
|
+
const hasContextBar = !!(hasMainLoop || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
|
|
509
530
|
|
|
510
531
|
return (
|
|
532
|
+
<>
|
|
511
533
|
<header
|
|
512
534
|
className="relative z-20 border-b border-white/[0.06] shrink-0"
|
|
513
535
|
style={{
|
|
@@ -586,8 +608,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
586
608
|
onClick={agent ? startRename : undefined}
|
|
587
609
|
title={agent ? 'Click to rename' : undefined}
|
|
588
610
|
>{
|
|
589
|
-
session.name
|
|
590
|
-
: session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
|
|
611
|
+
session.name.startsWith('agent-thread:') ? (agent?.name || session.name)
|
|
591
612
|
: session.name
|
|
592
613
|
}</span>
|
|
593
614
|
)}
|
|
@@ -673,6 +694,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
673
694
|
)}
|
|
674
695
|
{modelName && (
|
|
675
696
|
<div className="relative shrink-0" ref={modelSwitcherRef}>
|
|
697
|
+
<Tip label="Switch LLM model">
|
|
676
698
|
<button
|
|
677
699
|
type="button"
|
|
678
700
|
onClick={() => {
|
|
@@ -681,13 +703,13 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
681
703
|
}}
|
|
682
704
|
disabled={streaming}
|
|
683
705
|
className="inline-flex items-center gap-1 text-[11px] text-text-3/45 font-mono shrink-0 cursor-pointer bg-transparent border-none px-1 py-0.5 rounded-[5px] hover:bg-white/[0.04] hover:text-text-3/70 transition-colors disabled:cursor-default disabled:hover:text-text-3/45"
|
|
684
|
-
title="Switch model"
|
|
685
706
|
>
|
|
686
707
|
{modelName}
|
|
687
708
|
<svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="shrink-0 opacity-30">
|
|
688
709
|
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
689
710
|
</svg>
|
|
690
711
|
</button>
|
|
712
|
+
</Tip>
|
|
691
713
|
{modelSwitcherOpen && (
|
|
692
714
|
<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">
|
|
693
715
|
<div className="text-[10px] font-600 text-text-3/50 uppercase tracking-wider mb-2">Provider</div>
|
|
@@ -717,16 +739,17 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
717
739
|
)}
|
|
718
740
|
</div>
|
|
719
741
|
)}
|
|
742
|
+
<Tip label={`Open working directory: ${shortPath(session.cwd)}`}>
|
|
720
743
|
<button
|
|
721
744
|
type="button"
|
|
722
745
|
onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
|
|
723
746
|
className="inline-flex items-center shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-text-3/20 hover:text-text-3/50 hover:bg-white/[0.04] transition-colors"
|
|
724
|
-
title={shortPath(session.cwd)}
|
|
725
747
|
>
|
|
726
748
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
727
749
|
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
|
|
728
750
|
</svg>
|
|
729
751
|
</button>
|
|
752
|
+
</Tip>
|
|
730
753
|
{/* Live agent status */}
|
|
731
754
|
{(() => {
|
|
732
755
|
const liveStatus = agentStatus || (missionState.status ? {
|
|
@@ -776,12 +799,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
776
799
|
{/* Heartbeat compound control */}
|
|
777
800
|
{heartbeatSupported && (
|
|
778
801
|
<div className="flex items-center rounded-[8px] shrink-0" style={{ background: 'rgba(255,255,255,0.025)' }}>
|
|
802
|
+
<Tip label={heartbeatWillRun ? 'Disable heartbeat — periodic check-ins' : 'Enable heartbeat — periodic check-ins'}>
|
|
779
803
|
<button
|
|
780
804
|
onClick={handleToggleHeartbeat}
|
|
781
805
|
disabled={heartbeatSaving}
|
|
782
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
|
|
783
807
|
${heartbeatWillRun ? 'text-emerald-400 hover:bg-emerald-500/10' : 'text-text-3/60 hover:bg-white/[0.04]'}`}
|
|
784
|
-
title={heartbeatWillRun ? 'Disable heartbeat' : 'Enable heartbeat'}
|
|
785
808
|
>
|
|
786
809
|
<span className={`w-1.5 h-1.5 rounded-full transition-colors ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/30'}`} />
|
|
787
810
|
HB
|
|
@@ -789,18 +812,20 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
789
812
|
<span className="text-[9px] text-text-3/40">(bounded)</span>
|
|
790
813
|
)}
|
|
791
814
|
</button>
|
|
815
|
+
</Tip>
|
|
792
816
|
<div className="relative" ref={hbDropdownRef}>
|
|
817
|
+
<Tip label="Set heartbeat interval">
|
|
793
818
|
<button
|
|
794
819
|
onClick={() => setHbDropdownOpen((o) => !o)}
|
|
795
820
|
disabled={heartbeatSaving}
|
|
796
821
|
className="flex items-center gap-0.5 pl-1 pr-2 py-1 text-text-3/50 hover:text-text-3/70 hover:bg-white/[0.04] transition-colors cursor-pointer border-none"
|
|
797
|
-
title="Set heartbeat interval"
|
|
798
822
|
>
|
|
799
823
|
<span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
|
|
800
824
|
<svg width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="opacity-40">
|
|
801
825
|
<polyline points="6 9 12 15 18 9" />
|
|
802
826
|
</svg>
|
|
803
827
|
</button>
|
|
828
|
+
</Tip>
|
|
804
829
|
{hbDropdownOpen && (
|
|
805
830
|
<div className="absolute top-full right-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
|
|
806
831
|
{[...(typeof window !== 'undefined' && window.location.hostname === 'localhost' ? [10, 15, 30, 60] : []), 1800, 3600, 7200, 21600, 43200].map((sec) => (
|
|
@@ -886,53 +911,58 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
886
911
|
{/* Context bar: tools, mission controls, links */}
|
|
887
912
|
{hasContextBar && (
|
|
888
913
|
<div className="flex items-center gap-1.5 px-3.5 pb-1.5 flex-wrap">
|
|
889
|
-
{
|
|
914
|
+
{hasMainLoop && (
|
|
890
915
|
<>
|
|
891
|
-
<
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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>
|
|
901
927
|
|
|
902
|
-
<
|
|
903
|
-
onClick={handleToggleMissionMode}
|
|
904
|
-
disabled={mainLoopSaving}
|
|
905
|
-
className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600
|
|
906
|
-
${missionMode === 'autonomous' ? 'bg-indigo-500/12 hover:bg-indigo-500/20 text-indigo-300' : 'bg-white/[0.03] hover:bg-white/[0.06] text-text-3/60'}`}
|
|
907
|
-
title="Toggle autonomy mode"
|
|
908
|
-
>
|
|
909
|
-
{missionMode === 'autonomous' ? 'Auto' : 'Assist'}
|
|
910
|
-
</button>
|
|
911
|
-
<button
|
|
912
|
-
onClick={handleNudgeMission}
|
|
913
|
-
disabled={mainLoopSaving || missionPaused}
|
|
914
|
-
className="px-2 py-1 rounded-[7px] bg-blue-500/8 hover:bg-blue-500/15 text-blue-400 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600"
|
|
915
|
-
title="Run one tick"
|
|
916
|
-
>
|
|
917
|
-
Nudge
|
|
918
|
-
</button>
|
|
919
|
-
<button
|
|
920
|
-
onClick={handleSetMissionGoal}
|
|
921
|
-
disabled={mainLoopSaving}
|
|
922
|
-
className="px-2 py-1 rounded-[7px] bg-fuchsia-500/8 hover:bg-fuchsia-500/15 text-fuchsia-300 transition-colors cursor-pointer border-none text-[10px] font-600"
|
|
923
|
-
title="Set mission goal"
|
|
924
|
-
>
|
|
925
|
-
Goal
|
|
926
|
-
</button>
|
|
927
|
-
{missionEventsCount > 0 && (
|
|
928
|
+
<Tip label={missionMode === 'autonomous' ? 'Switch to assisted mode' : 'Switch to autonomous mode'}>
|
|
928
929
|
<button
|
|
929
|
-
onClick={
|
|
930
|
+
onClick={handleToggleMissionMode}
|
|
930
931
|
disabled={mainLoopSaving}
|
|
931
|
-
className=
|
|
932
|
-
|
|
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'}`}
|
|
933
934
|
>
|
|
934
|
-
|
|
935
|
+
{missionMode === 'autonomous' ? 'Auto' : 'Assist'}
|
|
935
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>
|
|
936
966
|
)}
|
|
937
967
|
<span className="text-[9px] text-text-3/40 uppercase tracking-wider shrink-0">
|
|
938
968
|
{missionStatus}{missionMomentum !== null ? ` · ${missionMomentum}` : ''}
|
|
@@ -942,6 +972,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
942
972
|
</>
|
|
943
973
|
)}
|
|
944
974
|
{hasMemoryLink && (
|
|
975
|
+
<Tip label="View agent memories">
|
|
945
976
|
<button
|
|
946
977
|
onClick={() => { setMemoryAgentFilter(session.agentId!); setActiveView('memory'); setSidebarOpen(true) }}
|
|
947
978
|
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"
|
|
@@ -951,9 +982,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
951
982
|
</svg>
|
|
952
983
|
Memories
|
|
953
984
|
</button>
|
|
985
|
+
</Tip>
|
|
954
986
|
)}
|
|
955
987
|
{hasSourceFilter && onConnectorFilterChange && connectorSources && (
|
|
956
988
|
<div className="relative shrink-0" ref={sourceDropdownRef}>
|
|
989
|
+
<Tip label="Filter messages by source connector">
|
|
957
990
|
<button
|
|
958
991
|
onClick={() => setSourceDropdownOpen((o) => !o)}
|
|
959
992
|
className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600 shrink-0 ${
|
|
@@ -961,7 +994,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
961
994
|
? 'bg-accent-soft/60 text-accent-bright/80 hover:bg-accent-soft'
|
|
962
995
|
: 'bg-white/[0.03] text-text-3/50 hover:bg-white/[0.06] hover:text-text-3/70'
|
|
963
996
|
}`}
|
|
964
|
-
title="Filter by message source"
|
|
965
997
|
>
|
|
966
998
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
967
999
|
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
|
|
@@ -973,6 +1005,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
973
1005
|
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
974
1006
|
</svg>
|
|
975
1007
|
</button>
|
|
1008
|
+
</Tip>
|
|
976
1009
|
{sourceDropdownOpen && (
|
|
977
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-[140px]">
|
|
978
1011
|
<button
|
|
@@ -1005,11 +1038,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
1005
1038
|
)}
|
|
1006
1039
|
{isOpenClawAgent && openclawSessionKey && (
|
|
1007
1040
|
<>
|
|
1041
|
+
<Tip label="Sync chat history from OpenClaw gateway">
|
|
1008
1042
|
<button
|
|
1009
1043
|
onClick={handleSyncHistory}
|
|
1010
1044
|
disabled={syncingHistory}
|
|
1011
1045
|
className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-indigo-500/8 hover:bg-indigo-500/12 transition-colors cursor-pointer border-none disabled:opacity-50 text-[10px] font-600 text-indigo-400 shrink-0"
|
|
1012
|
-
title="Sync from gateway"
|
|
1013
1046
|
>
|
|
1014
1047
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
1015
1048
|
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" /><path d="M3 3v5h5" />
|
|
@@ -1017,10 +1050,12 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
1017
1050
|
</svg>
|
|
1018
1051
|
{syncingHistory ? 'Syncing...' : 'Sync'}
|
|
1019
1052
|
</button>
|
|
1053
|
+
</Tip>
|
|
1020
1054
|
{syncResult && <span className="text-[9px] text-emerald-300/80 shrink-0">{syncResult}</span>}
|
|
1021
1055
|
</>
|
|
1022
1056
|
)}
|
|
1023
1057
|
{linkedTask && (
|
|
1058
|
+
<Tip label="View linked task">
|
|
1024
1059
|
<button
|
|
1025
1060
|
onClick={() => setActiveView('tasks')}
|
|
1026
1061
|
className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-amber-500/8 hover:bg-amber-500/12 transition-colors cursor-pointer text-[10px] font-600 text-amber-500 shrink-0"
|
|
@@ -1030,13 +1065,14 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
1030
1065
|
</svg>
|
|
1031
1066
|
<span className="truncate max-w-[160px]">{linkedTask.title}</span>
|
|
1032
1067
|
</button>
|
|
1068
|
+
</Tip>
|
|
1033
1069
|
)}
|
|
1034
1070
|
{resumeHandle && (
|
|
1035
1071
|
<div className="flex items-center rounded-[7px] bg-white/[0.03] group/resume shrink-0">
|
|
1072
|
+
<Tip label="Copy CLI resume command">
|
|
1036
1073
|
<button
|
|
1037
1074
|
onClick={handleCopySessionId}
|
|
1038
1075
|
className="flex items-center gap-1 px-2 py-1 rounded-l-[7px] hover:bg-white/[0.06] transition-colors cursor-pointer"
|
|
1039
|
-
title="Copy resume command"
|
|
1040
1076
|
>
|
|
1041
1077
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/40 shrink-0">
|
|
1042
1078
|
<path d="M4 17l6 0l0 -6" /><path d="M20 7l-6 0l0 6" /><path d="M4 17l10 -10" />
|
|
@@ -1045,22 +1081,24 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
1045
1081
|
{copied ? 'Copied!' : `${resumeHandle.label}: ${resumeHandle.id}`}
|
|
1046
1082
|
</span>
|
|
1047
1083
|
</button>
|
|
1084
|
+
</Tip>
|
|
1085
|
+
<Tip label="Dismiss resume handle">
|
|
1048
1086
|
<button
|
|
1049
1087
|
onClick={handleDismissResumeHandle}
|
|
1050
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"
|
|
1051
|
-
title="Dismiss"
|
|
1052
1089
|
>
|
|
1053
1090
|
<svg width="8" height="8" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40 hover:text-text-3">
|
|
1054
1091
|
<path d="M4 4l8 8M12 4l-8 8" />
|
|
1055
1092
|
</svg>
|
|
1056
1093
|
</button>
|
|
1094
|
+
</Tip>
|
|
1057
1095
|
</div>
|
|
1058
1096
|
)}
|
|
1059
1097
|
{browserActive && (
|
|
1098
|
+
<Tip label="Close the browser session">
|
|
1060
1099
|
<button
|
|
1061
1100
|
onClick={onStopBrowser}
|
|
1062
1101
|
className="flex items-center gap-1 px-2 py-1 rounded-[7px] bg-accent-bright/8 hover:bg-red-500/12 transition-colors cursor-pointer group text-[10px] font-600 shrink-0"
|
|
1063
|
-
title="Stop browser"
|
|
1064
1102
|
>
|
|
1065
1103
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-accent-bright group-hover:text-red-400">
|
|
1066
1104
|
<rect x="3" y="3" width="18" height="14" rx="2" /><path d="M3 9h18" />
|
|
@@ -1070,9 +1108,54 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
1070
1108
|
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
1071
1109
|
</svg>
|
|
1072
1110
|
</button>
|
|
1111
|
+
</Tip>
|
|
1073
1112
|
)}
|
|
1074
1113
|
</div>
|
|
1075
1114
|
)}
|
|
1115
|
+
|
|
1076
1116
|
</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
|
+
</>
|
|
1077
1160
|
)
|
|
1078
1161
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
|
-
import {
|
|
7
|
-
import { fetchMessages } from '@/lib/
|
|
6
|
+
import { ChatCard } from './chat-card'
|
|
7
|
+
import { fetchMessages } from '@/lib/chats'
|
|
8
8
|
import { toast } from 'sonner'
|
|
9
9
|
import { Skeleton } from '@/components/shared/skeleton'
|
|
10
10
|
import { EmptyState } from '@/components/shared/empty-state'
|
|
@@ -17,7 +17,7 @@ interface Props {
|
|
|
17
17
|
type SessionFilter = 'all' | 'active' | 'human' | 'orchestrated'
|
|
18
18
|
type SortMode = 'lastActive' | 'name' | 'messages'
|
|
19
19
|
|
|
20
|
-
export function
|
|
20
|
+
export function ChatList({ inSidebar, onSelect }: Props) {
|
|
21
21
|
const sessions = useAppStore((s) => s.sessions)
|
|
22
22
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
23
23
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
@@ -44,7 +44,6 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
44
44
|
|
|
45
45
|
const allUserSessions = useMemo(() => {
|
|
46
46
|
return Object.values(sessions).filter((s) => {
|
|
47
|
-
if (s.name === '__main__') return false
|
|
48
47
|
const owner = (s.user || '').toLowerCase()
|
|
49
48
|
const isPlatformOwned = owner === 'system' || owner === 'connector' || owner === 'swarm'
|
|
50
49
|
const isCurrentUserOwned = !!currentUser && owner === currentUser.toLowerCase()
|
|
@@ -186,7 +185,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
186
185
|
<div className="flex flex-col gap-1 px-2 pb-4">
|
|
187
186
|
{filtered.map((s) => (
|
|
188
187
|
<div key={s.id} className="group/pin relative">
|
|
189
|
-
<
|
|
188
|
+
<ChatCard
|
|
190
189
|
session={s}
|
|
191
190
|
active={s.id === currentSessionId}
|
|
192
191
|
onClick={() => handleSelect(s.id)}
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
-
import { AVAILABLE_TOOLS, PLATFORM_TOOLS
|
|
6
|
+
import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
|
|
7
7
|
import type { ToolDefinition } from '@/lib/tool-definitions'
|
|
8
8
|
import type { Session } from '@/types'
|
|
9
|
+
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@/components/ui/tooltip'
|
|
9
10
|
|
|
10
11
|
const TOOL_GROUPS: { label: string; tools: ToolDefinition[] }[] = [
|
|
11
12
|
{ label: 'Plugins', tools: AVAILABLE_TOOLS },
|
|
@@ -26,7 +27,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
26
27
|
const skills = useAppStore((s) => s.skills)
|
|
27
28
|
|
|
28
29
|
const agent = session.agentId ? agents[session.agentId] : null
|
|
29
|
-
const sessionTools: string[] = session.
|
|
30
|
+
const sessionTools: string[] = session.plugins || []
|
|
30
31
|
|
|
31
32
|
// Agent's skill IDs
|
|
32
33
|
const agentSkillIds: string[] = agent?.skillIds || []
|
|
@@ -44,7 +45,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
44
45
|
const updated = sessionTools.includes(toolId)
|
|
45
46
|
? sessionTools.filter((t) => t !== toolId)
|
|
46
47
|
: [...sessionTools, toolId]
|
|
47
|
-
await api('PUT', `/
|
|
48
|
+
await api('PUT', `/chats/${session.id}`, { plugins: updated })
|
|
48
49
|
loadSessions()
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -69,26 +70,33 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
69
70
|
{open && (
|
|
70
71
|
<div className="absolute top-full left-0 mt-1.5 w-[260px] max-h-[420px] overflow-y-auto rounded-[12px] border border-white/[0.08] shadow-xl z-[120] overflow-hidden"
|
|
71
72
|
style={{ animation: 'fade-in 0.15s ease', backgroundColor: '#171a2b' }}>
|
|
72
|
-
|
|
73
|
+
<TooltipProvider delayDuration={300}>
|
|
73
74
|
{TOOL_GROUPS.map((group, gi) => (
|
|
74
75
|
<div key={group.label} className={`px-3 pb-1 ${gi === 0 ? 'pt-3' : 'pt-1 border-t border-white/[0.04]'}`}>
|
|
75
76
|
<p className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider mb-2">{group.label}</p>
|
|
76
77
|
{group.tools.map((tool) => {
|
|
77
78
|
const enabled = sessionTools.includes(tool.id)
|
|
78
79
|
return (
|
|
79
|
-
<
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
<Tooltip key={tool.id}>
|
|
81
|
+
<TooltipTrigger asChild>
|
|
82
|
+
<label className="flex items-center gap-2.5 py-1.5 cursor-pointer">
|
|
83
|
+
<div
|
|
84
|
+
onClick={() => toggleTool(tool.id)}
|
|
85
|
+
className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
86
|
+
${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
87
|
+
>
|
|
88
|
+
<div className={`absolute top-[2px] w-[14px] h-[14px] rounded-full bg-white transition-all duration-200
|
|
89
|
+
${enabled ? 'left-[16px]' : 'left-[2px]'}`} />
|
|
90
|
+
</div>
|
|
91
|
+
<span className={`text-[12px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
|
|
92
|
+
{tool.label}
|
|
93
|
+
</span>
|
|
94
|
+
</label>
|
|
95
|
+
</TooltipTrigger>
|
|
96
|
+
<TooltipContent side="right" sideOffset={8} className="max-w-[200px] bg-[#1e2140] text-text-2 border border-white/[0.08] text-[11px] leading-snug px-2.5 py-1.5">
|
|
97
|
+
{tool.description}
|
|
98
|
+
</TooltipContent>
|
|
99
|
+
</Tooltip>
|
|
92
100
|
)
|
|
93
101
|
})}
|
|
94
102
|
</div>
|
|
@@ -110,6 +118,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
110
118
|
</div>
|
|
111
119
|
)}
|
|
112
120
|
|
|
121
|
+
</TooltipProvider>
|
|
113
122
|
<div className="px-3 py-2 border-t border-white/[0.04] bg-white/[0.02]">
|
|
114
123
|
<p className="text-[10px] text-text-3/70">Changes apply to the next message</p>
|
|
115
124
|
</div>
|