@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.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. 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-[#0d0f1a] flex flex-col h-full overflow-hidden fade-up-delay">
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
- className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap
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 agent={agent} />
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
- function OverviewTab({ agent }: { agent: Agent }) {
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: skills.filter((s) => s.source === source),
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">Loading skills...</div>
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 parsed = parseIdentityMd(content)
28
- setDraft({ name: parsed.name || '', creature: parsed.creature || '', vibe: parsed.vibe || '', emoji: parsed.emoji || '' })
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 parsed = parseUserMd(content)
31
- setDraft({ name: parsed.name || '', callThem: parsed.callThem || '', pronouns: parsed.pronouns || '', timezone: parsed.timezone || '', notes: parsed.notes || '', context: parsed.context || '' })
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 parsed = parseSoulMd(content)
34
- setDraft({ coreTruths: parsed.coreTruths || '', boundaries: parsed.boundaries || '', vibe: parsed.vibe || '', continuity: parsed.continuity || '' })
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
- <button
103
- onClick={handleSave}
104
- 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"
105
- style={{ fontFamily: 'inherit' }}
106
- >
107
- Apply to Raw Editor
108
- </button>
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: `${config.color}18`,
108
- border: `1px solid ${config.color}30`,
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: 'rgba(34,197,94,0.1)',
157
- border: '1px solid rgba(34,197,94,0.2)',
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">