@swarmclawai/swarmclaw 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -12,6 +12,11 @@ import { CronJobForm } from './cron-job-form'
|
|
|
12
12
|
|
|
13
13
|
interface Props {
|
|
14
14
|
agent: Agent
|
|
15
|
+
onEditAgent?: () => void
|
|
16
|
+
onClearHistory?: () => void
|
|
17
|
+
onDeleteAgent?: () => void
|
|
18
|
+
onDeleteChat?: () => void
|
|
19
|
+
isMainChat?: boolean
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
type InspectorTab = 'overview' | 'files' | 'skills' | 'automations' | 'advanced'
|
|
@@ -24,7 +29,7 @@ const TABS: { id: InspectorTab; label: string; openclawOnly?: boolean }[] = [
|
|
|
24
29
|
{ id: 'advanced', label: 'Advanced' },
|
|
25
30
|
]
|
|
26
31
|
|
|
27
|
-
export function InspectorPanel({ agent }: Props) {
|
|
32
|
+
export function InspectorPanel({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: Props) {
|
|
28
33
|
const inspectorTab = useAppStore((s) => s.inspectorTab)
|
|
29
34
|
const setInspectorTab = useAppStore((s) => s.setInspectorTab)
|
|
30
35
|
const setInspectorOpen = useAppStore((s) => s.setInspectorOpen)
|
|
@@ -52,7 +57,7 @@ export function InspectorPanel({ agent }: Props) {
|
|
|
52
57
|
const agentSchedules = Object.values(schedules).filter((s) => s.agentId === agent.id)
|
|
53
58
|
|
|
54
59
|
return (
|
|
55
|
-
<div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-
|
|
60
|
+
<div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-bg flex flex-col h-full overflow-hidden fade-up-delay">
|
|
56
61
|
{/* Header */}
|
|
57
62
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
58
63
|
<h3 className="font-display text-[14px] font-600 text-text truncate">{agent.name}</h3>
|
|
@@ -69,12 +74,14 @@ export function InspectorPanel({ agent }: Props) {
|
|
|
69
74
|
</div>
|
|
70
75
|
|
|
71
76
|
{/* Tab bar */}
|
|
72
|
-
<div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0">
|
|
77
|
+
<div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0" role="tablist">
|
|
73
78
|
{visibleTabs.map((tab) => (
|
|
74
79
|
<button
|
|
75
80
|
key={tab.id}
|
|
81
|
+
role="tab"
|
|
76
82
|
onClick={() => setInspectorTab(tab.id)}
|
|
77
|
-
|
|
83
|
+
aria-selected={inspectorTab === tab.id}
|
|
84
|
+
className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap focus-visible:ring-1 focus-visible:ring-accent-bright/50
|
|
78
85
|
${inspectorTab === tab.id
|
|
79
86
|
? 'bg-accent-soft text-accent-bright'
|
|
80
87
|
: 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
@@ -88,7 +95,14 @@ export function InspectorPanel({ agent }: Props) {
|
|
|
88
95
|
{/* Tab content */}
|
|
89
96
|
<div className="flex-1 min-h-0 overflow-y-auto">
|
|
90
97
|
{inspectorTab === 'overview' && (
|
|
91
|
-
<OverviewTab
|
|
98
|
+
<OverviewTab
|
|
99
|
+
agent={agent}
|
|
100
|
+
onEditAgent={onEditAgent}
|
|
101
|
+
onClearHistory={onClearHistory}
|
|
102
|
+
onDeleteAgent={onDeleteAgent}
|
|
103
|
+
onDeleteChat={onDeleteChat}
|
|
104
|
+
isMainChat={isMainChat}
|
|
105
|
+
/>
|
|
92
106
|
)}
|
|
93
107
|
{inspectorTab === 'files' && isOpenClaw && (
|
|
94
108
|
<AgentFilesEditor agentId={agent.id} />
|
|
@@ -117,7 +131,16 @@ export function InspectorPanel({ agent }: Props) {
|
|
|
117
131
|
)
|
|
118
132
|
}
|
|
119
133
|
|
|
120
|
-
|
|
134
|
+
interface OverviewTabProps {
|
|
135
|
+
agent: Agent
|
|
136
|
+
onEditAgent?: () => void
|
|
137
|
+
onClearHistory?: () => void
|
|
138
|
+
onDeleteAgent?: () => void
|
|
139
|
+
onDeleteChat?: () => void
|
|
140
|
+
isMainChat?: boolean
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function OverviewTab({ agent, onEditAgent, onClearHistory, onDeleteAgent, onDeleteChat, isMainChat }: OverviewTabProps) {
|
|
121
144
|
return (
|
|
122
145
|
<div className="p-4 flex flex-col gap-4">
|
|
123
146
|
<div>
|
|
@@ -160,6 +183,58 @@ function OverviewTab({ agent }: { agent: Agent }) {
|
|
|
160
183
|
</div>
|
|
161
184
|
</div>
|
|
162
185
|
)}
|
|
186
|
+
|
|
187
|
+
{/* Actions */}
|
|
188
|
+
{(onEditAgent || onClearHistory || onDeleteAgent || onDeleteChat) && (
|
|
189
|
+
<>
|
|
190
|
+
<div className="border-t border-white/[0.06] mt-2" />
|
|
191
|
+
<div className="flex flex-col gap-2">
|
|
192
|
+
{onEditAgent && (
|
|
193
|
+
<button
|
|
194
|
+
onClick={onEditAgent}
|
|
195
|
+
className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-accent-bright bg-accent-soft/50 border border-accent-bright/10 cursor-pointer transition-all hover:bg-accent-soft"
|
|
196
|
+
style={{ fontFamily: 'inherit' }}
|
|
197
|
+
>
|
|
198
|
+
Edit Agent
|
|
199
|
+
</button>
|
|
200
|
+
)}
|
|
201
|
+
{(onClearHistory || onDeleteAgent || onDeleteChat) && (
|
|
202
|
+
<>
|
|
203
|
+
<label className="block text-[11px] font-600 uppercase tracking-wider text-red-400/50 mt-2">Danger Zone</label>
|
|
204
|
+
<div className="flex flex-col gap-1.5">
|
|
205
|
+
{onClearHistory && (
|
|
206
|
+
<button
|
|
207
|
+
onClick={onClearHistory}
|
|
208
|
+
className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
|
|
209
|
+
style={{ fontFamily: 'inherit' }}
|
|
210
|
+
>
|
|
211
|
+
Clear History
|
|
212
|
+
</button>
|
|
213
|
+
)}
|
|
214
|
+
{onDeleteAgent && !isMainChat && (
|
|
215
|
+
<button
|
|
216
|
+
onClick={onDeleteAgent}
|
|
217
|
+
className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
|
|
218
|
+
style={{ fontFamily: 'inherit' }}
|
|
219
|
+
>
|
|
220
|
+
Delete Agent
|
|
221
|
+
</button>
|
|
222
|
+
)}
|
|
223
|
+
{onDeleteChat && !isMainChat && (
|
|
224
|
+
<button
|
|
225
|
+
onClick={onDeleteChat}
|
|
226
|
+
className="w-full px-3 py-2 rounded-[8px] text-[12px] font-600 text-red-400/80 bg-red-400/[0.04] border border-red-400/[0.08] cursor-pointer transition-all hover:bg-red-400/[0.08] hover:text-red-400 text-left"
|
|
227
|
+
style={{ fontFamily: 'inherit' }}
|
|
228
|
+
>
|
|
229
|
+
Delete Chat
|
|
230
|
+
</button>
|
|
231
|
+
)}
|
|
232
|
+
</div>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</>
|
|
237
|
+
)}
|
|
163
238
|
</div>
|
|
164
239
|
)
|
|
165
240
|
}
|
|
@@ -23,6 +23,7 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
|
|
|
23
23
|
const [saving, setSaving] = useState(false)
|
|
24
24
|
const [installTarget, setInstallTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
25
25
|
const [removeTarget, setRemoveTarget] = useState<OpenClawSkillEntry | null>(null)
|
|
26
|
+
const [readinessFilter, setReadinessFilter] = useState<'all' | 'ready' | 'needs-setup'>('all')
|
|
26
27
|
|
|
27
28
|
const loadSkills = useCallback(async () => {
|
|
28
29
|
setLoading(true)
|
|
@@ -67,15 +68,24 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
|
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
const readyCount = skills.filter((s) => s.eligible).length
|
|
72
|
+
const needsSetupCount = skills.filter((s) => !s.eligible).length
|
|
73
|
+
|
|
74
|
+
const filteredSkills = readinessFilter === 'all'
|
|
75
|
+
? skills
|
|
76
|
+
: readinessFilter === 'ready'
|
|
77
|
+
? skills.filter((s) => s.eligible)
|
|
78
|
+
: skills.filter((s) => !s.eligible)
|
|
79
|
+
|
|
70
80
|
const grouped = SOURCE_ORDER
|
|
71
81
|
.map((source) => ({
|
|
72
82
|
source,
|
|
73
|
-
items:
|
|
83
|
+
items: filteredSkills.filter((s) => s.source === source),
|
|
74
84
|
}))
|
|
75
85
|
.filter((g) => g.items.length > 0)
|
|
76
86
|
|
|
77
87
|
if (loading) {
|
|
78
|
-
return <div className="flex items-center justify-center h-32 text-[13px] text-text-3/50"
|
|
88
|
+
return <div className="flex items-center justify-center gap-2 h-32 text-[13px] text-text-3/50"><span className="w-3 h-3 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin" />Loading skills...</div>
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
if (error) {
|
|
@@ -90,7 +100,7 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
|
|
|
90
100
|
<button
|
|
91
101
|
key={m}
|
|
92
102
|
onClick={() => handleModeChange(m)}
|
|
93
|
-
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
|
|
103
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
|
|
94
104
|
${mode === m ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
95
105
|
style={{ fontFamily: 'inherit' }}
|
|
96
106
|
>
|
|
@@ -99,6 +109,25 @@ export function OpenClawSkillsPanel({ agentId, initialMode = 'all', initialAllow
|
|
|
99
109
|
))}
|
|
100
110
|
</div>
|
|
101
111
|
|
|
112
|
+
{/* Readiness filter */}
|
|
113
|
+
<div className="flex gap-1">
|
|
114
|
+
{([
|
|
115
|
+
{ key: 'all' as const, label: `All (${skills.length})` },
|
|
116
|
+
{ key: 'ready' as const, label: `Ready (${readyCount})` },
|
|
117
|
+
{ key: 'needs-setup' as const, label: `Needs Setup (${needsSetupCount})` },
|
|
118
|
+
]).map((f) => (
|
|
119
|
+
<button
|
|
120
|
+
key={f.key}
|
|
121
|
+
onClick={() => setReadinessFilter(f.key)}
|
|
122
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all focus-visible:ring-1 focus-visible:ring-accent-bright/50
|
|
123
|
+
${readinessFilter === f.key ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
|
|
124
|
+
style={{ fontFamily: 'inherit' }}
|
|
125
|
+
>
|
|
126
|
+
{f.label}
|
|
127
|
+
</button>
|
|
128
|
+
))}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
102
131
|
{/* Skill groups */}
|
|
103
132
|
{grouped.map(({ source, items }) => (
|
|
104
133
|
<div key={source}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
3
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
4
4
|
import type { PersonalityDraft } from '@/types'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import {
|
|
@@ -21,22 +21,33 @@ const labelClass = 'block text-[11px] font-600 uppercase tracking-wider text-tex
|
|
|
21
21
|
|
|
22
22
|
export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSave }: Props) {
|
|
23
23
|
const [draft, setDraft] = useState<Record<string, string>>({})
|
|
24
|
+
const [initialDraft, setInitialDraft] = useState<Record<string, string>>({})
|
|
25
|
+
const [saveState, setSaveState] = useState<'idle' | 'saved'>('idle')
|
|
24
26
|
|
|
25
27
|
useEffect(() => {
|
|
28
|
+
let parsed: Record<string, string> = {}
|
|
26
29
|
if (fileType === 'IDENTITY.md') {
|
|
27
|
-
const
|
|
28
|
-
|
|
30
|
+
const p = parseIdentityMd(content)
|
|
31
|
+
parsed = { name: p.name || '', creature: p.creature || '', vibe: p.vibe || '', emoji: p.emoji || '' }
|
|
29
32
|
} else if (fileType === 'USER.md') {
|
|
30
|
-
const
|
|
31
|
-
|
|
33
|
+
const p = parseUserMd(content)
|
|
34
|
+
parsed = { name: p.name || '', callThem: p.callThem || '', pronouns: p.pronouns || '', timezone: p.timezone || '', notes: p.notes || '', context: p.context || '' }
|
|
32
35
|
} else if (fileType === 'SOUL.md') {
|
|
33
|
-
const
|
|
34
|
-
|
|
36
|
+
const p = parseSoulMd(content)
|
|
37
|
+
parsed = { coreTruths: p.coreTruths || '', boundaries: p.boundaries || '', vibe: p.vibe || '', continuity: p.continuity || '' }
|
|
35
38
|
}
|
|
39
|
+
setDraft(parsed)
|
|
40
|
+
setInitialDraft(parsed)
|
|
41
|
+
setSaveState('idle')
|
|
36
42
|
}, [content, fileType])
|
|
37
43
|
|
|
44
|
+
const isDirty = useMemo(() => {
|
|
45
|
+
return Object.keys(draft).some((k) => draft[k] !== (initialDraft[k] ?? ''))
|
|
46
|
+
}, [draft, initialDraft])
|
|
47
|
+
|
|
38
48
|
const update = (key: string, value: string) => {
|
|
39
49
|
setDraft((prev) => ({ ...prev, [key]: value }))
|
|
50
|
+
setSaveState('idle')
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
const handleSave = () => {
|
|
@@ -49,6 +60,9 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
|
|
|
49
60
|
serialized = serializeSoulMd(draft as PersonalityDraft['soul'])
|
|
50
61
|
}
|
|
51
62
|
onSave(serialized)
|
|
63
|
+
setInitialDraft({ ...draft })
|
|
64
|
+
setSaveState('saved')
|
|
65
|
+
setTimeout(() => setSaveState('idle'), 1500)
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
const fields = fileType === 'IDENTITY.md'
|
|
@@ -99,13 +113,27 @@ export function PersonalityBuilder({ agentId: _agentId, fileType, content, onSav
|
|
|
99
113
|
)}
|
|
100
114
|
</div>
|
|
101
115
|
))}
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
116
|
+
<div className="flex items-center gap-3">
|
|
117
|
+
<button
|
|
118
|
+
onClick={handleSave}
|
|
119
|
+
className="self-start px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer transition-all hover:brightness-110 focus-visible:ring-1 focus-visible:ring-accent-bright/50"
|
|
120
|
+
style={{ fontFamily: 'inherit' }}
|
|
121
|
+
>
|
|
122
|
+
Apply to Raw Editor
|
|
123
|
+
</button>
|
|
124
|
+
{isDirty && saveState === 'idle' && (
|
|
125
|
+
<span className="inline-flex items-center gap-1.5 text-[11px] text-amber-400 font-600">
|
|
126
|
+
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
|
127
|
+
Unsaved
|
|
128
|
+
</span>
|
|
129
|
+
)}
|
|
130
|
+
{saveState === 'saved' && (
|
|
131
|
+
<span className="inline-flex items-center gap-1.5 text-[11px] text-emerald-400 font-600">
|
|
132
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
133
|
+
Saved
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
109
137
|
</div>
|
|
110
138
|
)
|
|
111
139
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
5
|
+
import { SOUL_LIBRARY, SOUL_ARCHETYPES, searchSouls, type SoulTemplate } from '@/lib/soul-library'
|
|
6
|
+
|
|
7
|
+
interface SoulLibraryPickerProps {
|
|
8
|
+
open: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
onSelect: (soul: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function SoulLibraryPicker({ open, onClose, onSelect }: SoulLibraryPickerProps) {
|
|
14
|
+
const [query, setQuery] = useState('')
|
|
15
|
+
const [archetype, setArchetype] = useState('All')
|
|
16
|
+
|
|
17
|
+
const results = useMemo(() => searchSouls(query, archetype), [query, archetype])
|
|
18
|
+
|
|
19
|
+
const handleSelect = (template: SoulTemplate) => {
|
|
20
|
+
onSelect(template.soul)
|
|
21
|
+
onClose()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
26
|
+
<div className="mb-6">
|
|
27
|
+
<h2 className="font-display text-[24px] font-700 tracking-[-0.03em] mb-1">Soul Library</h2>
|
|
28
|
+
<p className="text-[13px] text-text-3">Browse personality templates for your agent</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
{/* Search */}
|
|
32
|
+
<div className="mb-4">
|
|
33
|
+
<input
|
|
34
|
+
type="text"
|
|
35
|
+
value={query}
|
|
36
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
37
|
+
placeholder="Search personalities..."
|
|
38
|
+
className="w-full px-4 py-3 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[14px] outline-none focus-glow"
|
|
39
|
+
style={{ fontFamily: 'inherit' }}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{/* Archetype filter tabs */}
|
|
44
|
+
<div className="flex gap-1 flex-wrap mb-6">
|
|
45
|
+
{SOUL_ARCHETYPES.map((a) => (
|
|
46
|
+
<button
|
|
47
|
+
key={a}
|
|
48
|
+
onClick={() => setArchetype(a)}
|
|
49
|
+
className={`px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
|
|
50
|
+
${archetype === a
|
|
51
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
52
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
53
|
+
style={{ fontFamily: 'inherit' }}
|
|
54
|
+
>
|
|
55
|
+
{a}
|
|
56
|
+
</button>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
{/* Results grid */}
|
|
61
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 max-h-[60vh] overflow-y-auto pb-4">
|
|
62
|
+
{results.map((template) => (
|
|
63
|
+
<button
|
|
64
|
+
key={template.id}
|
|
65
|
+
onClick={() => handleSelect(template)}
|
|
66
|
+
className="text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 hover:border-accent-bright/20 transition-all cursor-pointer group"
|
|
67
|
+
style={{ fontFamily: 'inherit' }}
|
|
68
|
+
>
|
|
69
|
+
<div className="flex items-start gap-2 mb-2">
|
|
70
|
+
<h4 className="text-[14px] font-600 text-text group-hover:text-accent-bright transition-colors">
|
|
71
|
+
{template.name}
|
|
72
|
+
</h4>
|
|
73
|
+
<span className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] text-text-3 text-[10px] font-600 shrink-0">
|
|
74
|
+
{template.archetype}
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
<p className="text-[12px] text-text-3 mb-2">{template.description}</p>
|
|
78
|
+
<p className="text-[11px] text-text-3/60 line-clamp-2 italic">{template.soul}</p>
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
{results.length === 0 && (
|
|
82
|
+
<p className="text-[13px] text-text-3 col-span-2 text-center py-8">No personalities match your search</p>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<p className="text-[11px] text-text-3/50 mt-4 text-center">{SOUL_LIBRARY.length} personalities available</p>
|
|
87
|
+
</BottomSheet>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from 'react'
|
|
4
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
6
|
+
|
|
7
|
+
interface CanvasPanelProps {
|
|
8
|
+
sessionId: string
|
|
9
|
+
agentName?: string
|
|
10
|
+
onClose: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
|
|
14
|
+
const [content, setContent] = useState<string | null>(null)
|
|
15
|
+
|
|
16
|
+
const loadCanvas = useCallback(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const res = await api<{ content: string | null }>('GET', `/canvas/${sessionId}`)
|
|
19
|
+
setContent(res.content)
|
|
20
|
+
} catch { /* ignore */ }
|
|
21
|
+
}, [sessionId])
|
|
22
|
+
|
|
23
|
+
useEffect(() => { loadCanvas() }, [loadCanvas]) // eslint-disable-line react-hooks/set-state-in-effect
|
|
24
|
+
useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
|
|
25
|
+
|
|
26
|
+
if (!content) return (
|
|
27
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
28
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
29
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
30
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
|
|
31
|
+
</svg>
|
|
32
|
+
<span className="text-[13px] font-600 text-text flex-1 truncate">
|
|
33
|
+
Canvas{agentName ? ` — ${agentName}` : ''}
|
|
34
|
+
</span>
|
|
35
|
+
<button
|
|
36
|
+
onClick={onClose}
|
|
37
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
38
|
+
title="Close canvas"
|
|
39
|
+
aria-label="Close canvas"
|
|
40
|
+
>
|
|
41
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
42
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
43
|
+
</svg>
|
|
44
|
+
</button>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex-1 flex items-center justify-center">
|
|
47
|
+
<div className="text-center">
|
|
48
|
+
<div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
|
|
49
|
+
<span className="text-[13px] text-text-3">Loading canvas...</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
|
|
57
|
+
{/* Toolbar */}
|
|
58
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
59
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
|
|
60
|
+
<rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
|
|
61
|
+
</svg>
|
|
62
|
+
<span className="text-[13px] font-600 text-text flex-1 truncate">
|
|
63
|
+
Canvas{agentName ? ` — ${agentName}` : ''}
|
|
64
|
+
</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={loadCanvas}
|
|
67
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
68
|
+
title="Refresh"
|
|
69
|
+
>
|
|
70
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
71
|
+
<polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
72
|
+
</svg>
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
onClick={onClose}
|
|
76
|
+
className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
|
|
77
|
+
title="Close canvas"
|
|
78
|
+
>
|
|
79
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
80
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
81
|
+
</svg>
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Sandboxed iframe */}
|
|
86
|
+
<div className="flex-1 overflow-hidden">
|
|
87
|
+
<iframe
|
|
88
|
+
sandbox="allow-scripts allow-same-origin"
|
|
89
|
+
srcDoc={content}
|
|
90
|
+
className="w-full h-full border-none bg-white"
|
|
91
|
+
title="Agent Canvas"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -11,6 +11,8 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
|
|
|
11
11
|
delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
|
|
12
12
|
delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
|
|
13
13
|
delegate_to_opencode_cli: { label: 'Delegated to OpenCode', color: '#38BDF8', icon: 'delegate' },
|
|
14
|
+
delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
|
|
15
|
+
check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
|
|
14
16
|
web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
|
|
15
17
|
connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
|
|
16
18
|
}
|
|
@@ -23,6 +25,8 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
|
|
|
23
25
|
if (toolName === 'manage_tasks' && parsed.title) return parsed.title
|
|
24
26
|
if (toolName === 'manage_schedules' && parsed.name) return parsed.name
|
|
25
27
|
if (toolName === 'manage_agents' && parsed.name) return parsed.name
|
|
28
|
+
if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
|
|
29
|
+
if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
|
|
26
30
|
if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
|
|
27
31
|
if (toolName === 'web_search' && parsed.query) return parsed.query
|
|
28
32
|
if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
|
|
@@ -104,8 +108,8 @@ export function ActivityMoment({ toolName, toolInput, onDismiss }: Props) {
|
|
|
104
108
|
<div
|
|
105
109
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
|
|
106
110
|
style={{
|
|
107
|
-
background:
|
|
108
|
-
border: `1px solid ${config.color}
|
|
111
|
+
background: 'var(--card)',
|
|
112
|
+
border: `1px solid ${config.color}40`,
|
|
109
113
|
}}
|
|
110
114
|
>
|
|
111
115
|
<MomentIcon icon={config.icon} color={config.color} />
|
|
@@ -153,8 +157,8 @@ export function HeartbeatMoment({ onDismiss }: { onDismiss: () => void }) {
|
|
|
153
157
|
<div
|
|
154
158
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
|
|
155
159
|
style={{
|
|
156
|
-
background: '
|
|
157
|
-
border: '1px solid rgba(34,197,94,0.
|
|
160
|
+
background: 'var(--card)',
|
|
161
|
+
border: '1px solid rgba(34,197,94,0.3)',
|
|
158
162
|
}}
|
|
159
163
|
>
|
|
160
164
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="#22c55e">
|