@swarmclawai/swarmclaw 0.6.7 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -88,7 +88,7 @@ export function ProjectList() {
88
88
 
89
89
  if (!filtered.length && !search) {
90
90
  return (
91
- <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
91
+ <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
92
92
  <div className="w-14 h-14 rounded-[16px] bg-accent-soft flex items-center justify-center mb-1">
93
93
  <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-accent-bright">
94
94
  <path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
@@ -117,7 +117,7 @@ export function ProjectList() {
117
117
  return (
118
118
  <div className="flex-1 flex flex-col h-full overflow-hidden">
119
119
  {/* Header with search and new button */}
120
- <div className="px-5 pt-5 pb-3 shrink-0">
120
+ <div className="px-5 pt-5 pb-3 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
121
121
  <div className="flex items-center justify-between mb-4">
122
122
  <div>
123
123
  <h2 className="font-display text-[20px] font-700 text-text tracking-[-0.02em]">Projects</h2>
@@ -159,7 +159,7 @@ export function ProjectList() {
159
159
  {/* Project cards */}
160
160
  <div className="flex-1 overflow-y-auto px-5 pb-5">
161
161
  <div className="grid gap-3">
162
- {filtered.map((project) => {
162
+ {filtered.map((project, idx) => {
163
163
  const stats = statsMap[project.id] || { agents: 0, tasks: 0, completedTasks: 0, schedules: 0, lastActivity: project.updatedAt }
164
164
  const isActive = activeProjectFilter === project.id
165
165
  const progressPct = stats.tasks > 0 ? Math.round((stats.completedTasks / stats.tasks) * 100) : 0
@@ -170,11 +170,15 @@ export function ProjectList() {
170
170
  className={`group relative rounded-[14px] border transition-all duration-200 cursor-pointer overflow-hidden
171
171
  ${isActive
172
172
  ? 'bg-white/[0.06] border-accent-bright/30 shadow-[0_0_20px_rgba(99,102,241,0.08)]'
173
- : 'bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05] hover:border-white/[0.1]'}`}
173
+ : 'bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05] hover:border-white/[0.1] hover:scale-[1.01]'}`}
174
174
  onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
175
+ style={{
176
+ animation: 'fade-up 0.4s var(--ease-spring) both',
177
+ animationDelay: `${0.1 + idx * 0.03}s`
178
+ }}
175
179
  >
176
180
  {/* Color accent stripe */}
177
- <div className="absolute left-0 top-0 bottom-0 w-1 rounded-l-[14px]" style={{ backgroundColor: project.color || '#6B7280' }} />
181
+ <div className="absolute left-0 top-0 bottom-0 w-1 rounded-l-[14px]" style={{ backgroundColor: project.color || '#6B7280', animation: 'spring-in 0.6s var(--ease-spring)' }} />
178
182
 
179
183
  <div className="pl-5 pr-4 py-4">
180
184
  <div className="flex items-start justify-between gap-3">
@@ -182,7 +186,7 @@ export function ProjectList() {
182
186
  <div className="flex items-center gap-2">
183
187
  <h3 className="font-display text-[14px] font-600 text-text truncate">{project.name}</h3>
184
188
  {isActive && (
185
- <span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
189
+ <span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright bg-accent-soft px-1.5 py-0.5 rounded-[5px]" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }}>
186
190
  active filter
187
191
  </span>
188
192
  )}
@@ -234,14 +238,18 @@ export function ProjectList() {
234
238
  {/* Progress bar */}
235
239
  {stats.tasks > 0 && (
236
240
  <div className="mt-3 flex items-center gap-2.5">
237
- <div className="flex-1 h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
241
+ <div className="flex-1 h-1.5 rounded-full bg-white/[0.06] overflow-hidden relative">
238
242
  <div
239
- className="h-full rounded-full transition-all duration-500"
243
+ className="h-full rounded-full transition-all duration-500 relative"
240
244
  style={{
241
245
  width: `${progressPct}%`,
242
246
  backgroundColor: progressPct === 100 ? '#22C55E' : (project.color || '#6366F1'),
243
247
  }}
244
- />
248
+ >
249
+ {isActive && progressPct < 100 && (
250
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-[shimmer-bar_2s_infinite]" />
251
+ )}
252
+ </div>
245
253
  </div>
246
254
  <span className={`text-[10px] font-mono font-600 ${progressPct === 100 ? 'text-emerald-400' : 'text-text-3/50'}`}>
247
255
  {progressPct}%
@@ -75,12 +75,24 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
75
75
  return (
76
76
  <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
77
77
  <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
78
- {allItems.map((item) => (
79
- <button
78
+ {allItems.map((item, idx) => (
79
+ <div
80
80
  key={item.id}
81
+ role="button"
82
+ tabIndex={0}
81
83
  onClick={() => handleEdit(item.id)}
84
+ onKeyDown={(e) => {
85
+ if (e.key === 'Enter' || e.key === ' ') {
86
+ e.preventDefault()
87
+ handleEdit(item.id)
88
+ }
89
+ }}
82
90
  className="w-full text-left p-4 rounded-[14px] border transition-all duration-200
83
- cursor-pointer hover:bg-surface-2 bg-surface border-white/[0.06]"
91
+ cursor-pointer hover:bg-white/[0.02] bg-surface border-white/[0.06] hover:border-white/[0.12] hover:scale-[1.01]"
92
+ style={{
93
+ animation: 'spring-in 0.5s var(--ease-spring) both',
94
+ animationDelay: `${idx * 0.05}s`
95
+ }}
84
96
  >
85
97
  <div className="flex items-center justify-between mb-1.5">
86
98
  <span className="font-display text-[14px] font-600 text-text truncate">{item.name}</span>
@@ -97,7 +109,9 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
97
109
  ${item.isEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
98
110
  >
99
111
  <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
100
- ${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`} />
112
+ ${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`}
113
+ style={item.isEnabled ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
114
+ />
101
115
  </div>
102
116
  <button
103
117
  onClick={(e) => handleDelete(e, item.id)}
@@ -110,7 +124,8 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
110
124
  </button>
111
125
  </>
112
126
  )}
113
- <span className={`w-2 h-2 rounded-full ${item.isConnected ? 'bg-emerald-400' : 'bg-white/10'}`} />
127
+ <span className={`w-2 h-2 rounded-full ${item.isConnected ? 'bg-emerald-400' : 'bg-white/10'}`}
128
+ style={item.isConnected ? { animation: 'pulse-subtle 2s infinite' } : undefined} />
114
129
  </div>
115
130
  </div>
116
131
  <div className="text-[12px] text-text-3/60 font-mono truncate">
@@ -121,7 +136,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
121
136
  </>
122
137
  )}
123
138
  </div>
124
- </button>
139
+ </div>
125
140
  ))}
126
141
  </div>
127
142
  </div>
@@ -120,13 +120,17 @@ export function ProviderSheet() {
120
120
  if (result.ok) {
121
121
  setTestStatus('pass')
122
122
  setTestMessage(result.message)
123
+ toast.success('Connection successful')
123
124
  } else {
124
125
  setTestStatus('fail')
125
126
  setTestMessage(result.message)
127
+ toast.error(result.message || 'Connection failed')
126
128
  }
127
129
  } catch (err: unknown) {
130
+ const msg = err instanceof Error ? err.message : 'Connection test failed'
128
131
  setTestStatus('fail')
129
- setTestMessage(err instanceof Error ? err.message : 'Connection test failed')
132
+ setTestMessage(msg)
133
+ toast.error(msg)
130
134
  }
131
135
  }
132
136
 
@@ -136,37 +140,50 @@ export function ProviderSheet() {
136
140
  }
137
141
 
138
142
  const handleSave = async () => {
139
- if (isBuiltin) {
140
- // Save model overrides for built-in providers
143
+ try {
144
+ if (isBuiltin) {
145
+ // Save model overrides for built-in providers
146
+ const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
147
+ await api('PUT', `/providers/${editingId}/models`, { models: modelList })
148
+ toast.success('Built-in provider models updated')
149
+ await loadProviders()
150
+ onClose()
151
+ return
152
+ }
141
153
  const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
142
- await api('PUT', `/providers/${editingId}/models`, { models: modelList })
143
- await loadProviders()
154
+ const data = {
155
+ name: name.trim() || 'Custom Provider',
156
+ baseUrl: baseUrl.trim(),
157
+ models: modelList,
158
+ requiresApiKey,
159
+ credentialId,
160
+ isEnabled,
161
+ }
162
+ if (editingCustom) {
163
+ await updateProviderConfig(editingCustom.id, data)
164
+ toast.success('Provider updated')
165
+ } else {
166
+ await createProviderConfig(data)
167
+ toast.success('Provider created')
168
+ }
169
+ await loadProviderConfigs()
144
170
  onClose()
145
- return
146
- }
147
- const modelList = models.split(',').map((m) => m.trim()).filter(Boolean)
148
- const data = {
149
- name: name.trim() || 'Custom Provider',
150
- baseUrl: baseUrl.trim(),
151
- models: modelList,
152
- requiresApiKey,
153
- credentialId,
154
- isEnabled,
155
- }
156
- if (editingCustom) {
157
- await updateProviderConfig(editingCustom.id, data)
158
- } else {
159
- await createProviderConfig(data)
171
+ } catch (err: unknown) {
172
+ toast.error(err instanceof Error ? err.message : 'Failed to save provider')
160
173
  }
161
- await loadProviderConfigs()
162
- onClose()
163
174
  }
164
175
 
165
176
  const handleDelete = async () => {
166
177
  if (editingCustom) {
167
- await deleteProviderConfig(editingCustom.id)
168
- await loadProviderConfigs()
169
- onClose()
178
+ if (!confirm(`Delete custom provider "${editingCustom.name}"?`)) return
179
+ try {
180
+ await deleteProviderConfig(editingCustom.id)
181
+ toast.success('Provider deleted')
182
+ await loadProviderConfigs()
183
+ onClose()
184
+ } catch (err: unknown) {
185
+ toast.error(err instanceof Error ? err.message : 'Failed to delete provider')
186
+ }
170
187
  }
171
188
  }
172
189
 
@@ -43,14 +43,12 @@ export function RunList() {
43
43
  try {
44
44
  const res = await api<SessionRunRecord[]>('GET', '/runs?limit=200')
45
45
  setRuns(Array.isArray(res) ? res : [])
46
- } catch {
47
- // ignore
48
- } finally {
49
- setLoading(false)
50
- }
46
+ } catch { /* ignore */ }
47
+ setLoading(false)
51
48
  }, [])
52
49
 
53
50
  useEffect(() => {
51
+ // eslint-disable-next-line react-hooks/set-state-in-effect
54
52
  fetchRuns()
55
53
  }, [fetchRuns])
56
54
 
@@ -61,6 +59,7 @@ export function RunList() {
61
59
  if (loading) {
62
60
  return (
63
61
  <div className="flex-1 flex items-center justify-center text-text-3 text-[13px]">
62
+ <span className="w-4 h-4 rounded-full border-2 border-text-3/20 border-t-text-3/60 animate-spin mr-2" />
64
63
  Loading runs...
65
64
  </div>
66
65
  )
@@ -69,7 +68,7 @@ export function RunList() {
69
68
  return (
70
69
  <div className="flex-1 flex flex-col overflow-hidden">
71
70
  {/* Controls */}
72
- <div className="px-5 py-2 space-y-2 shrink-0">
71
+ <div className="px-5 py-2 space-y-2 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
73
72
  {/* Status filter + auto-refresh */}
74
73
  <div className="flex items-center gap-1.5 flex-wrap">
75
74
  <button
@@ -94,33 +93,38 @@ export function RunList() {
94
93
  <div className="flex-1" />
95
94
  <button
96
95
  onClick={() => setAutoRefresh(!autoRefresh)}
97
- className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
96
+ className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none flex items-center gap-1.5 ${
98
97
  autoRefresh ? 'bg-green-500/10 text-green-400' : 'bg-white/[0.04] text-text-3'
99
98
  }`}
100
99
  >
100
+ {autoRefresh && <span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />}
101
101
  {autoRefresh ? 'LIVE' : 'PAUSED'}
102
102
  </button>
103
103
  </div>
104
104
  </div>
105
105
 
106
106
  {/* Count */}
107
- <div className="px-5 py-1 text-[10px] text-text-3/60">
107
+ <div className="px-5 py-1 text-[10px] text-text-3/60" style={{ animation: 'fade-in 0.6s ease 0.1s both' }}>
108
108
  {filtered.length} run{filtered.length !== 1 ? 's' : ''}
109
109
  </div>
110
110
 
111
111
  {/* Run list */}
112
112
  <div className="flex-1 overflow-y-auto px-4 pb-8">
113
113
  {filtered.length === 0 ? (
114
- <div className="flex items-center justify-center h-32 text-text-3 text-[12px]">
114
+ <div className="flex items-center justify-center h-32 text-text-3 text-[12px]" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
115
115
  No runs found
116
116
  </div>
117
117
  ) : (
118
118
  <div className="space-y-1">
119
- {filtered.map((run) => (
119
+ {filtered.map((run, idx) => (
120
120
  <button
121
121
  key={run.id}
122
122
  onClick={() => setSelected(run)}
123
- className="w-full text-left p-3 rounded-[10px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer block"
123
+ className="w-full text-left p-3 rounded-[10px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer block hover:scale-[1.01] active:scale-[0.99]"
124
+ style={{
125
+ animation: 'fade-up 0.4s var(--ease-spring) both',
126
+ animationDelay: `${0.1 + idx * 0.02}s`
127
+ }}
124
128
  >
125
129
  <div className="flex items-center gap-2 mb-1">
126
130
  <span className={`text-[9px] font-700 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px] ${STATUS_COLORS[run.status].bg} ${STATUS_COLORS[run.status].text}`}>
@@ -144,7 +148,7 @@ export function RunList() {
144
148
  {/* Detail Sheet */}
145
149
  <BottomSheet open={!!selected} onClose={() => setSelected(null)}>
146
150
  {selected && (
147
- <>
151
+ <div style={{ animation: 'fade-in 0.3s ease' }}>
148
152
  <div className="mb-6">
149
153
  <div className="flex items-center gap-3 mb-3">
150
154
  <span className={`text-[11px] font-700 uppercase tracking-wider px-2.5 py-1 rounded-[6px] ${STATUS_COLORS[selected.status].bg} ${STATUS_COLORS[selected.status].text}`}>
@@ -214,7 +218,7 @@ export function RunList() {
214
218
  </pre>
215
219
  </div>
216
220
  )}
217
- </>
221
+ </div>
218
222
  )}
219
223
  </BottomSheet>
220
224
  </div>
@@ -27,9 +27,10 @@ function formatNext(ts?: number): string {
27
27
  interface Props {
28
28
  schedule: Schedule
29
29
  inSidebar?: boolean
30
+ index?: number
30
31
  }
31
32
 
32
- export function ScheduleCard({ schedule, inSidebar }: Props) {
33
+ export function ScheduleCard({ schedule, inSidebar, index = 0 }: Props) {
33
34
  const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
34
35
  const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
35
36
  const loadSchedules = useAppStore((s) => s.loadSchedules)
@@ -62,7 +63,11 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
62
63
  onClick={handleClick}
63
64
  className="relative py-3.5 px-4 cursor-pointer rounded-[14px]
64
65
  transition-all duration-200 active:scale-[0.98]
65
- bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03]"
66
+ bg-transparent border border-transparent hover:bg-white/[0.02] hover:border-white/[0.03] hover:scale-[1.01]"
67
+ style={{
68
+ animation: 'spring-in 0.5s var(--ease-spring) both',
69
+ animationDelay: `${Math.min(index * 0.05, 0.4)}s`
70
+ }}
66
71
  >
67
72
  <div className="flex items-center gap-2.5">
68
73
  <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{schedule.name}</span>
@@ -74,7 +79,9 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
74
79
  ${schedule.status === 'active' ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
75
80
  >
76
81
  <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
77
- ${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`} />
82
+ ${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`}
83
+ style={schedule.status === 'active' ? { animation: 'spring-in 0.3s var(--ease-spring)' } : undefined}
84
+ />
78
85
  </div>
79
86
  )}
80
87
  <span className={`text-[10px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${statusClass}`}>
@@ -118,8 +118,8 @@ export function ScheduleList({ inSidebar }: Props) {
118
118
  ? 'flex flex-col gap-1 px-2 pb-4'
119
119
  : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 px-5 pb-6'
120
120
  }>
121
- {filtered.map((s) => (
122
- <ScheduleCard key={s.id} schedule={s} inSidebar={inSidebar} />
121
+ {filtered.map((s, idx) => (
122
+ <ScheduleCard key={s.id} schedule={s} inSidebar={inSidebar} index={idx} />
123
123
  ))}
124
124
  </div>
125
125
  </div>
@@ -10,6 +10,8 @@ import type { ScheduleType, ScheduleStatus } from '@/types'
10
10
  import cronstrue from 'cronstrue'
11
11
  import { SectionLabel } from '@/components/shared/section-label'
12
12
  import { SCHEDULE_TEMPLATES, type ScheduleTemplate } from '@/lib/schedule-templates'
13
+ import { HintTip } from '@/components/shared/hint-tip'
14
+ import { toast } from 'sonner'
13
15
  import {
14
16
  Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
15
17
  Activity, ShieldCheck, DatabaseBackup, FileText,
@@ -176,20 +178,31 @@ export function ScheduleSheet() {
176
178
  runAt: scheduleType === 'once' ? Date.now() + intervalMs : undefined,
177
179
  status,
178
180
  }
179
- if (editing) {
180
- await updateSchedule(editing.id, data)
181
- } else {
182
- await createSchedule(data)
181
+ try {
182
+ if (editing) {
183
+ await updateSchedule(editing.id, data)
184
+ toast.success('Schedule updated successfully')
185
+ } else {
186
+ await createSchedule(data)
187
+ toast.success('Schedule created successfully')
188
+ }
189
+ await loadSchedules()
190
+ onClose()
191
+ } catch (err: unknown) {
192
+ toast.error(err instanceof Error ? err.message : 'Failed to save schedule')
183
193
  }
184
- await loadSchedules()
185
- onClose()
186
194
  }
187
195
 
188
196
  const handleDelete = async () => {
189
- if (editing) {
197
+ if (!editing) return
198
+ if (!confirm(`Delete schedule "${editing.name}"?`)) return
199
+ try {
190
200
  await deleteSchedule(editing.id)
201
+ toast.success('Schedule deleted')
191
202
  await loadSchedules()
192
203
  onClose()
204
+ } catch (err: unknown) {
205
+ toast.error(err instanceof Error ? err.message : 'Failed to delete schedule')
193
206
  }
194
207
  }
195
208
 
@@ -326,7 +339,10 @@ export function ScheduleSheet() {
326
339
  {step === whenStep && (
327
340
  <div>
328
341
  <div className="mb-8">
329
- <SectionLabel>Schedule Type</SectionLabel>
342
+ <div className="flex items-center gap-2 mb-3">
343
+ <SectionLabel className="mb-0">Schedule Type</SectionLabel>
344
+ <HintTip text="Once: runs a single time. Interval: repeats every N minutes. Cron: advanced scheduling with cron syntax" />
345
+ </div>
330
346
  <div className="grid grid-cols-3 gap-3">
331
347
  {(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
332
348
  <button
@@ -347,7 +363,10 @@ export function ScheduleSheet() {
347
363
 
348
364
  {scheduleType === 'cron' && (
349
365
  <div className="mb-8">
350
- <SectionLabel>Schedule</SectionLabel>
366
+ <div className="flex items-center gap-2 mb-3">
367
+ <SectionLabel className="mb-0">Schedule</SectionLabel>
368
+ <HintTip text="Standard cron format: minute hour day month weekday (e.g. 0 9 * * 1-5 = weekdays at 9am)" />
369
+ </div>
351
370
 
352
371
  {/* Preset buttons */}
353
372
  <div className="flex flex-wrap gap-2 mb-4">
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { AgentAvatar } from '@/components/agents/agent-avatar'
7
7
  import { api } from '@/lib/api-client'
8
+ import { toast } from 'sonner'
8
9
 
9
10
  const inputClass = 'w-full px-4 py-3 rounded-[14px] bg-bg border border-white/[0.06] text-text text-[14px] outline-none focus:border-accent-bright/40 transition-colors placeholder:text-text-3/70'
10
11
 
@@ -64,6 +65,7 @@ export function SecretSheet() {
64
65
  scope,
65
66
  agentIds: scope === 'agent' ? agentIds : [],
66
67
  })
68
+ toast.success('Secret updated')
67
69
  } else {
68
70
  await api('POST', '/secrets', {
69
71
  name: name.trim(),
@@ -72,11 +74,12 @@ export function SecretSheet() {
72
74
  scope,
73
75
  agentIds: scope === 'agent' ? agentIds : [],
74
76
  })
77
+ toast.success('Secret created')
75
78
  }
76
79
  await loadSecrets()
77
80
  handleClose()
78
81
  } catch (err: unknown) {
79
- console.error('Failed to save secret:', err instanceof Error ? err.message : String(err))
82
+ toast.error(err instanceof Error ? err.message : 'Failed to save secret')
80
83
  } finally {
81
84
  setSaving(false)
82
85
  }
@@ -84,12 +87,14 @@ export function SecretSheet() {
84
87
 
85
88
  const handleDelete = async () => {
86
89
  if (!editing) return
90
+ if (!confirm(`Delete secret "${editing.name}"?`)) return
87
91
  try {
88
92
  await api('DELETE', `/secrets/${editing.id}`)
93
+ toast.success('Secret deleted')
89
94
  await loadSecrets()
90
95
  handleClose()
91
96
  } catch (err: unknown) {
92
- console.error('Failed to delete secret:', err instanceof Error ? err.message : String(err))
97
+ toast.error(err instanceof Error ? err.message : 'Failed to delete secret')
93
98
  }
94
99
  }
95
100
 
@@ -53,7 +53,7 @@ export function SecretsList({ inSidebar }: Props) {
53
53
  return (
54
54
  <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
55
55
  <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
56
- {secretList.map((secret) => {
56
+ {secretList.map((secret, idx) => {
57
57
  const scopeLabel = secret.scope === 'global'
58
58
  ? 'Global'
59
59
  : `${secret.agentIds.length} agent(s)`
@@ -61,15 +61,28 @@ export function SecretsList({ inSidebar }: Props) {
61
61
  ? secret.agentIds.map((id) => agents[id]).filter(Boolean)
62
62
  : []
63
63
  return (
64
- <button
64
+ <div
65
65
  key={secret.id}
66
+ role="button"
67
+ tabIndex={0}
66
68
  onClick={() => {
67
69
  setEditingSecretId(secret.id)
68
70
  setSecretSheetOpen(true)
69
71
  }}
72
+ onKeyDown={(e) => {
73
+ if (e.key === 'Enter' || e.key === ' ') {
74
+ e.preventDefault()
75
+ setEditingSecretId(secret.id)
76
+ setSecretSheetOpen(true)
77
+ }
78
+ }}
70
79
  className="w-full text-left p-4 rounded-[14px] bg-surface border border-white/[0.06]
71
- hover:border-white/[0.1] cursor-pointer transition-all group"
72
- style={{ fontFamily: 'inherit' }}
80
+ hover:border-white/[0.12] hover:bg-white/[0.02] hover:scale-[1.01] cursor-pointer transition-all group"
81
+ style={{
82
+ fontFamily: 'inherit',
83
+ animation: 'spring-in 0.5s var(--ease-spring) both',
84
+ animationDelay: `${idx * 0.05}s`
85
+ }}
73
86
  >
74
87
  <div className="flex items-center gap-2.5 mb-1">
75
88
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
@@ -110,7 +123,7 @@ export function SecretsList({ inSidebar }: Props) {
110
123
  )}
111
124
  </div>
112
125
  )}
113
- </button>
126
+ </div>
114
127
  )
115
128
  })}
116
129
  </div>