@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
@@ -3,6 +3,7 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import { setStoredAccessKey } from '@/lib/api-client'
5
5
  import { fetchWithTimeout } from '@/lib/fetch-timeout'
6
+ import { copyTextToClipboard } from '@/lib/clipboard'
6
7
 
7
8
  interface AccessKeyGateProps {
8
9
  onAuthenticated: () => void
@@ -42,7 +43,8 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
42
43
 
43
44
  const handleCopyKey = async () => {
44
45
  try {
45
- await navigator.clipboard.writeText(generatedKey)
46
+ const copiedKey = await copyTextToClipboard(generatedKey)
47
+ if (!copiedKey) return
46
48
  setCopied(true)
47
49
  setTimeout(() => setCopied(false), 2000)
48
50
  } catch {
@@ -121,12 +123,9 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
121
123
  />
122
124
  </div>
123
125
 
124
- <div
125
- className="relative max-w-[440px] w-full text-center"
126
- style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}
127
- >
126
+ <div className="relative max-w-[440px] w-full text-center">
128
127
  {/* Lock / Key icon */}
129
- <div className="flex justify-center mb-6">
128
+ <div className="flex justify-center mb-6" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
130
129
  <div className="relative w-12 h-12 flex items-center justify-center">
131
130
  <svg
132
131
  width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor"
@@ -151,15 +150,17 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
151
150
  {firstTime ? (
152
151
  /* ── First-time setup: show the generated key ── */
153
152
  <>
154
- <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
155
- Your Access Key
156
- </h1>
157
- <p className="text-[14px] text-text-2 mb-8">
158
- This key was generated for your server. Copy it somewhere safe — you&apos;ll need it to connect from other devices.
159
- </p>
153
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
154
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
155
+ Your Access Key
156
+ </h1>
157
+ <p className="text-[14px] text-text-2 mb-8">
158
+ This key was generated for your server. Copy it somewhere safe — you&apos;ll need it to connect from other devices.
159
+ </p>
160
+ </div>
160
161
 
161
162
  {/* Key display */}
162
- <div className="mb-3">
163
+ <div className="mb-3" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both' }}>
163
164
  <div
164
165
  className="inline-flex items-center gap-3 px-5 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface
165
166
  cursor-pointer hover:border-accent-bright/20 transition-all duration-200"
@@ -185,7 +186,7 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
185
186
  </div>
186
187
  </div>
187
188
 
188
- <div className="relative h-5 mb-8">
189
+ <div className="relative h-5 mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
189
190
  <p
190
191
  className="absolute inset-x-0 text-[12px] transition-all duration-300"
191
192
  style={{
@@ -207,56 +208,64 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
207
208
  </p>
208
209
  </div>
209
210
 
210
- <button
211
- onClick={handleClaimKey}
212
- disabled={loading}
213
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
214
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
215
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
216
- >
217
- {loading ? 'Connecting...' : 'Continue'}
218
- </button>
211
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
212
+ <button
213
+ onClick={handleClaimKey}
214
+ disabled={loading}
215
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
216
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
217
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
218
+ >
219
+ {loading ? 'Connecting...' : 'Continue'}
220
+ </button>
221
+ </div>
219
222
  </>
220
223
  ) : (
221
224
  /* ── Returning user: enter key ── */
222
225
  <>
223
- <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
224
- Connect
225
- </h1>
226
- <p className="text-[14px] text-text-2 mb-2">
227
- Enter the access key to connect to this server.
228
- </p>
229
- <p className="text-[12px] text-text-3 mb-8">
230
- You can find it in <code className="text-text-2">.env.local</code> in the project root.
231
- </p>
226
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
227
+ <h1 className="font-display text-[36px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
228
+ Connect
229
+ </h1>
230
+ <p className="text-[14px] text-text-2 mb-2">
231
+ Enter the access key to connect to this server.
232
+ </p>
233
+ <p className="text-[12px] text-text-3 mb-8">
234
+ You can find it in <code className="text-text-2">.env.local</code> in the project root.
235
+ </p>
236
+ </div>
232
237
 
233
238
  <form onSubmit={handleSubmit} className="flex flex-col items-center gap-4">
234
- <input
235
- type="password"
236
- value={key}
237
- onChange={(e) => { setKey(e.target.value); setError('') }}
238
- placeholder="Access key"
239
- autoFocus
240
- autoComplete="off"
241
- className="w-full max-w-[320px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
242
- text-text text-[16px] text-center font-mono outline-none
243
- transition-all duration-200 placeholder:text-text-3/70
244
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
245
- />
239
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both', width: '100%', display: 'flex', justifyContent: 'center' }}>
240
+ <input
241
+ type="password"
242
+ value={key}
243
+ onChange={(e) => { setKey(e.target.value); setError('') }}
244
+ placeholder="Access key"
245
+ autoFocus
246
+ autoComplete="off"
247
+ className="w-full max-w-[320px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
248
+ text-text text-[16px] text-center font-mono outline-none
249
+ transition-all duration-200 placeholder:text-text-3/70
250
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
251
+ />
252
+ </div>
246
253
 
247
254
  {error && (
248
- <p className="text-[13px] text-red-400">{error}</p>
255
+ <p className="text-[13px] text-red-400" style={{ animation: 'ai-shake 0.5s' }}>{error}</p>
249
256
  )}
250
257
 
251
- <button
252
- type="submit"
253
- disabled={!key.trim() || loading}
254
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
255
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
256
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
257
- >
258
- {loading ? 'Connecting...' : 'Connect'}
259
- </button>
258
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
259
+ <button
260
+ type="submit"
261
+ disabled={!key.trim() || loading}
262
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
263
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
264
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
265
+ >
266
+ {loading ? 'Connecting...' : 'Connect'}
267
+ </button>
268
+ </div>
260
269
  </form>
261
270
  </>
262
271
  )}
@@ -39,11 +39,10 @@ export function UserPicker() {
39
39
  }} />
40
40
  </div>
41
41
 
42
- <div className="relative max-w-[420px] w-full text-center"
43
- style={{ animation: 'fade-in 0.6s cubic-bezier(0.16, 1, 0.3, 1)' }}>
42
+ <div className="relative max-w-[420px] w-full text-center">
44
43
 
45
44
  {/* Sparkle icon */}
46
- <div className="flex justify-center mb-6">
45
+ <div className="flex justify-center mb-6" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
47
46
  <div className="relative w-12 h-12">
48
47
  <svg width="48" height="48" viewBox="0 0 48 48" fill="none" className="text-accent-bright"
49
48
  style={{ animation: 'sparkle-spin 8s linear infinite' }}>
@@ -54,29 +53,33 @@ export function UserPicker() {
54
53
  </div>
55
54
  </div>
56
55
 
57
- <h1 className="font-display text-[42px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
58
- Welcome
59
- </h1>
60
- <p className="text-[15px] text-text-2 mb-10">
61
- What should we call you?
62
- </p>
56
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.1s both' }}>
57
+ <h1 className="font-display text-[42px] font-800 leading-[1.05] tracking-[-0.04em] mb-3">
58
+ Welcome
59
+ </h1>
60
+ <p className="text-[15px] text-text-2 mb-10">
61
+ What should we call you?
62
+ </p>
63
+ </div>
63
64
 
64
65
  <form onSubmit={handleSubmit} className="flex flex-col items-center gap-5">
65
- <input
66
- type="text"
67
- value={name}
68
- onChange={(e) => setName(e.target.value)}
69
- placeholder="Your name"
70
- autoFocus
71
- className="w-full max-w-[280px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
72
- text-text text-[18px] text-center font-display font-600 outline-none
73
- transition-all duration-200 placeholder:text-text-3/70
74
- focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
75
- style={{ fontFamily: 'inherit' }}
76
- />
66
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.2s both', width: '100%', display: 'flex', justifyContent: 'center' }}>
67
+ <input
68
+ type="text"
69
+ value={name}
70
+ onChange={(e) => setName(e.target.value)}
71
+ placeholder="Your name"
72
+ autoFocus
73
+ className="w-full max-w-[280px] px-6 py-4 rounded-[16px] border border-white/[0.08] bg-surface
74
+ text-text text-[18px] text-center font-display font-600 outline-none
75
+ transition-all duration-200 placeholder:text-text-3/70
76
+ focus:border-accent-bright/30 focus:shadow-[0_0_30px_rgba(99,102,241,0.1)]"
77
+ style={{ fontFamily: 'inherit' }}
78
+ />
79
+ </div>
77
80
 
78
81
  {/* Avatar picker */}
79
- <div className="flex flex-col items-center gap-3">
82
+ <div className="flex flex-col items-center gap-3" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
80
83
  <AgentAvatar seed={avatarSeed || null} name={name || '?'} size={64} />
81
84
  <div className="flex items-center gap-2">
82
85
  <input
@@ -99,16 +102,18 @@ export function UserPicker() {
99
102
  </div>
100
103
  </div>
101
104
 
102
- <button
103
- type="submit"
104
- disabled={!name.trim()}
105
- className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
106
- cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
107
- shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
108
- style={{ fontFamily: 'inherit' }}
109
- >
110
- Get Started
111
- </button>
105
+ <div style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
106
+ <button
107
+ type="submit"
108
+ disabled={!name.trim()}
109
+ className="px-12 py-4 rounded-[16px] border-none bg-accent-bright text-white text-[16px] font-display font-600
110
+ cursor-pointer hover:brightness-110 active:scale-[0.97] transition-all duration-200
111
+ shadow-[0_6px_28px_rgba(99,102,241,0.3)] disabled:opacity-30"
112
+ style={{ fontFamily: 'inherit' }}
113
+ >
114
+ Get Started
115
+ </button>
116
+ </div>
112
117
  </form>
113
118
  </div>
114
119
  </div>
@@ -7,6 +7,7 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
7
7
  memory_tool: { label: 'Committed to memory', color: '#A855F7', icon: 'brain' },
8
8
  manage_tasks: { label: 'Created a task', color: '#EC4899', icon: 'clipboard' },
9
9
  manage_schedules: { label: 'Scheduled something', color: '#EC4899', icon: 'clipboard' },
10
+ schedule_wake: { label: 'Set a reminder', color: '#F59E0B', icon: 'clipboard' },
10
11
  manage_agents: { label: 'Created an agent', color: '#EC4899', icon: 'clipboard' },
11
12
  delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
12
13
  delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
@@ -24,6 +25,7 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
24
25
  if ((toolName === 'memory' || toolName === 'memory_tool') && parsed.key) return parsed.key
25
26
  if (toolName === 'manage_tasks' && parsed.title) return parsed.title
26
27
  if (toolName === 'manage_schedules' && parsed.name) return parsed.name
28
+ if (toolName === 'schedule_wake' && parsed.message) return parsed.message
27
29
  if (toolName === 'manage_agents' && parsed.name) return parsed.name
28
30
  if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
29
31
  if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
@@ -21,6 +21,7 @@ import { HeartbeatHistoryPanel } from './heartbeat-history-panel'
21
21
  import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
22
22
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
23
23
  import { speak } from '@/lib/tts'
24
+ import { api } from '@/lib/api-client'
24
25
 
25
26
  const PROMPT_SUGGESTIONS = [
26
27
  { text: 'What can you help me with?', icon: 'book', gradient: 'from-[#6366F1]/10 to-[#818CF8]/5' },
@@ -66,6 +67,15 @@ export function ChatArea() {
66
67
  const [heartbeatHistoryOpen, setHeartbeatHistoryOpen] = useState(false)
67
68
  const [messagesLoading, setMessagesLoading] = useState(true)
68
69
  const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
70
+ const [pluginChatActions, setPluginChatActions] = useState<Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>>([])
71
+
72
+ useEffect(() => {
73
+ if (sessionId) {
74
+ api<Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>>('GET', '/plugins/ui?type=chat_actions').then(actions => {
75
+ if (Array.isArray(actions)) setPluginChatActions(actions)
76
+ }).catch(() => {})
77
+ }
78
+ }, [sessionId])
69
79
 
70
80
  // Collect unique connector sources from messages for filter UI
71
81
  const { connectorSources, hasDirectMessages } = useMemo(() => {
@@ -421,6 +431,7 @@ export function ChatArea() {
421
431
  streaming={streamingForThisSession}
422
432
  onSend={sendMessage}
423
433
  onStop={stopStreaming}
434
+ pluginChatActions={pluginChatActions}
424
435
  />
425
436
 
426
437
  <Dropdown open={menuOpen} onClose={() => setMenuOpen(false)}>
@@ -5,7 +5,6 @@ import type { Session } from '@/types'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
7
  import { IconButton } from '@/components/shared/icon-button'
8
- import { UsageBadge } from '@/components/shared/usage-badge'
9
8
  import { ChatToolToggles } from './chat-tool-toggles'
10
9
  import { api } from '@/lib/api-client'
11
10
  import {
@@ -17,6 +16,7 @@ import { AgentAvatar } from '@/components/agents/agent-avatar'
17
16
  import { ModelCombobox } from '@/components/shared/model-combobox'
18
17
  import { toast } from 'sonner'
19
18
  import type { ProviderType } from '@/types'
19
+ import { copyTextToClipboard } from '@/lib/clipboard'
20
20
  import { useWs } from '@/hooks/use-ws'
21
21
 
22
22
  function shortPath(p: string): string {
@@ -67,7 +67,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
67
67
  const toggleSound = useChatStore((s) => s.toggleSound)
68
68
  const debugOpen = useChatStore((s) => s.debugOpen)
69
69
  const setDebugOpen = useChatStore((s) => s.setDebugOpen)
70
- const lastUsage = useChatStore((s) => s.lastUsage)
71
70
  const agentStatus = useChatStore((s) => s.agentStatus)
72
71
  const agents = useAppStore((s) => s.agents)
73
72
  const tasks = useAppStore((s) => s.tasks)
@@ -109,18 +108,50 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
109
108
  const renameContainerRef = useRef<HTMLSpanElement>(null)
110
109
  const setWalletPanelAgentId = useAppStore((s) => s.setWalletPanelAgentId)
111
110
  const [walletBalance, setWalletBalance] = useState<number | null>(null)
111
+ const [headerWidgets, setHeaderWidgets] = useState<Array<{ id: string; label: string; icon?: string }>>([])
112
+
113
+ useEffect(() => {
114
+ api<Array<{ id: string; label: string; icon?: string }>>('GET', `/plugins/ui?type=header&sessionId=${session.id}`).then(widgets => {
115
+ if (Array.isArray(widgets)) setHeaderWidgets(widgets)
116
+ }).catch(() => {})
117
+ }, [session.id])
112
118
 
113
119
  const fetchWalletBalance = useCallback(async () => {
114
- if (!agent?.walletId) { setWalletBalance(null); return }
120
+ if (!agent?.walletId) {
121
+ setWalletBalance(null)
122
+ return
123
+ }
115
124
  try {
116
125
  const data = await api<{ balanceSol?: number }>('GET', `/wallets/${agent.walletId}`)
117
126
  setWalletBalance(data.balanceSol ?? null)
118
- } catch { setWalletBalance(null) }
127
+ } catch {
128
+ setWalletBalance(null)
129
+ }
119
130
  }, [agent?.walletId])
120
131
 
121
- useEffect(() => { fetchWalletBalance() }, [fetchWalletBalance])
132
+ useEffect(() => {
133
+ void fetchWalletBalance()
134
+ }, [fetchWalletBalance])
122
135
  useWs('wallets', fetchWalletBalance)
123
136
 
137
+
138
+ const visibleHeaderWidgets = useMemo(() => {
139
+ const seen = new Set<string>()
140
+ return headerWidgets.filter((widget) => {
141
+ const key = widget.id || widget.label
142
+ if (seen.has(key)) return false
143
+ seen.add(key)
144
+ return true
145
+ })
146
+ }, [headerWidgets])
147
+
148
+ const handleHeaderWidgetClick = (widgetId: string) => {
149
+ if (widgetId === 'wallet-status') {
150
+ if (agent?.id) setWalletPanelAgentId(agent.id)
151
+ setActiveView('wallets')
152
+ }
153
+ }
154
+
124
155
  // Find linked task for this session
125
156
  const linkedTask = useMemo(() => {
126
157
  return Object.values(tasks).find((t) => t.sessionId === session.id)
@@ -156,9 +187,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
156
187
 
157
188
  const handleCopySessionId = () => {
158
189
  if (!resumeHandle) return
159
- navigator.clipboard.writeText(resumeHandle.command)
160
- setCopied(true)
161
- setTimeout(() => setCopied(false), 2000)
190
+ void copyTextToClipboard(resumeHandle.command).then((copiedCommand) => {
191
+ if (!copiedCommand) return
192
+ setCopied(true)
193
+ setTimeout(() => setCopied(false), 2000)
194
+ })
162
195
  }
163
196
 
164
197
  const handleDismissResumeHandle = async (e: React.MouseEvent) => {
@@ -606,21 +639,37 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
606
639
  {/* Metadata tray: wallet · model · path · status */}
607
640
  <div className="flex items-center gap-1.5 min-w-0 overflow-hidden">
608
641
  <span className="text-text-3/10 text-[10px] select-none shrink-0">/</span>
609
- {walletBalance !== null && (
610
- <>
642
+ {visibleHeaderWidgets.map((widget) => {
643
+ const actionable = widget.id === 'wallet-status'
644
+ const walletLabel = walletBalance !== null
645
+ ? `${walletBalance.toFixed(3)} SOL`
646
+ : (widget.label || 'Wallet')
647
+ return (
611
648
  <button
649
+ key={widget.id}
612
650
  type="button"
613
- onClick={() => { setWalletPanelAgentId(agent!.id); setActiveView('wallets') }}
614
- className="inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] cursor-pointer text-[11px] text-text-3/45 font-mono hover:text-text-3/70 hover:bg-white/[0.04] transition-colors"
615
- title="View wallet"
651
+ onClick={actionable ? () => handleHeaderWidgetClick(widget.id) : undefined}
652
+ className={`inline-flex items-center gap-1 shrink-0 bg-transparent border-none p-0.5 rounded-[4px] text-[11px] font-mono transition-colors ${
653
+ actionable ? 'cursor-pointer text-text-3/45 hover:text-text-3/70 hover:bg-white/[0.04]' : 'cursor-default text-text-3/55'
654
+ }`}
655
+ title={actionable ? 'View wallet' : widget.label}
616
656
  >
617
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
618
- <rect x="2" y="6" width="20" height="14" rx="2" /><path d="M22 10H18a2 2 0 0 0 0 4h4" />
619
- </svg>
620
- {walletBalance.toFixed(3)} SOL
657
+ {actionable ? (
658
+ <>
659
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
660
+ <rect x="2" y="6" width="20" height="14" rx="2" />
661
+ <path d="M22 10H18a2 2 0 0 0 0 4h4" />
662
+ </svg>
663
+ {walletLabel}
664
+ </>
665
+ ) : (
666
+ widget.label
667
+ )}
621
668
  </button>
622
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
623
- </>
669
+ )
670
+ })}
671
+ {visibleHeaderWidgets.length > 0 && (
672
+ <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
624
673
  )}
625
674
  {modelName && (
626
675
  <div className="relative shrink-0" ref={modelSwitcherRef}>
@@ -668,12 +717,6 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
668
717
  )}
669
718
  </div>
670
719
  )}
671
- {lastUsage && !streaming && (
672
- <>
673
- <span className="text-text-3/10 text-[10px] select-none shrink-0">·</span>
674
- <UsageBadge {...lastUsage} />
675
- </>
676
- )}
677
720
  <button
678
721
  type="button"
679
722
  onClick={() => { api('POST', '/files/open', { path: session.cwd }).catch(() => {}) }}
@@ -855,6 +898,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
855
898
  <span className={`w-1.5 h-1.5 rounded-full ${missionPaused ? 'bg-amber-300' : 'bg-emerald-400'}`} />
856
899
  {missionPaused ? 'Paused' : 'Live'}
857
900
  </button>
901
+
858
902
  <button
859
903
  onClick={handleToggleMissionMode}
860
904
  disabled={mainLoopSaving}
@@ -8,8 +8,8 @@ import type { ToolDefinition } from '@/lib/tool-definitions'
8
8
  import type { Session } from '@/types'
9
9
 
10
10
  const TOOL_GROUPS: { label: string; tools: ToolDefinition[] }[] = [
11
- { label: 'Tools', tools: AVAILABLE_TOOLS },
12
- { label: 'Platform', tools: PLATFORM_TOOLS },
11
+ { label: 'Plugins', tools: AVAILABLE_TOOLS },
12
+ { label: 'Platform Plugins', tools: PLATFORM_TOOLS },
13
13
  ]
14
14
 
15
15
  const TOTAL_TOOL_COUNT = AVAILABLE_TOOLS.length + PLATFORM_TOOLS.length
@@ -0,0 +1,112 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { toast } from 'sonner'
7
+
8
+ interface Checkpoint {
9
+ checkpointId: string
10
+ parentCheckpointId?: string
11
+ metadata: Record<string, unknown>
12
+ createdAt: number
13
+ values?: Record<string, unknown>
14
+ }
15
+
16
+ interface Props {
17
+ sessionId: string
18
+ }
19
+
20
+ export function CheckpointTimeline({ sessionId }: Props) {
21
+ const [checkpoints, setCheckpoints] = useState<Checkpoint[]>([])
22
+ const [loading, setLoading] = useState(true)
23
+ const [restoringId, setRestoringId] = useState<string | null>(null)
24
+ const loadSessions = useAppStore((s) => s.loadSessions)
25
+
26
+ const load = async () => {
27
+ setLoading(true)
28
+ try {
29
+ const data = await api<Checkpoint[]>('GET', `/sessions/${sessionId}/checkpoints`)
30
+ setCheckpoints(data)
31
+ } catch (err) {
32
+ console.error('Failed to load checkpoints', err)
33
+ } finally {
34
+ setLoading(false)
35
+ }
36
+ }
37
+
38
+ useEffect(() => {
39
+ load()
40
+ // eslint-disable-next-line react-hooks/exhaustive-deps
41
+ }, [sessionId])
42
+
43
+ const handleRestore = async (checkpoint: Checkpoint) => {
44
+ if (!confirm('Restore session to this point? This will delete all subsequent history.')) return
45
+
46
+ setRestoringId(checkpoint.checkpointId)
47
+ try {
48
+ await api('POST', `/sessions/${sessionId}/restore`, {
49
+ checkpointId: checkpoint.checkpointId,
50
+ timestamp: checkpoint.createdAt
51
+ })
52
+ toast.success('Session restored successfully')
53
+ await loadSessions()
54
+ await load()
55
+ } catch (err) {
56
+ toast.error('Failed to restore session')
57
+ console.error(err)
58
+ } finally {
59
+ setRestoringId(null)
60
+ }
61
+ }
62
+
63
+ if (loading) {
64
+ return <div className="p-8 text-center text-text-3 text-[13px]">Retrieving history...</div>
65
+ }
66
+
67
+ if (checkpoints.length === 0) {
68
+ return (
69
+ <div className="p-8 text-center">
70
+ <p className="text-text-3 text-[13px]">No checkpoints found for this session.</p>
71
+ <p className="text-[11px] text-text-3/50 mt-1">Only LangGraph-orchestrated sessions support time travel.</p>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ return (
77
+ <div className="flex flex-col gap-3 p-5">
78
+ {checkpoints.map((cp, i) => (
79
+ <div
80
+ key={cp.checkpointId}
81
+ className="group relative flex flex-col gap-2 p-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04] transition-all"
82
+ >
83
+ <div className="flex items-center justify-between">
84
+ <div className="flex flex-col">
85
+ <span className="text-[11px] font-700 text-accent-bright uppercase tracking-wider">
86
+ {i === 0 ? 'Current State' : `Point ${checkpoints.length - i}`}
87
+ </span>
88
+ <span className="text-[10px] text-text-3 font-mono">
89
+ {new Date(cp.createdAt).toLocaleString()}
90
+ </span>
91
+ </div>
92
+ {i > 0 && (
93
+ <button
94
+ onClick={() => handleRestore(cp)}
95
+ disabled={!!restoringId}
96
+ className="px-3 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600 border-none cursor-pointer hover:brightness-110 disabled:opacity-50"
97
+ >
98
+ {restoringId === cp.checkpointId ? 'Restoring...' : 'Restore here'}
99
+ </button>
100
+ )}
101
+ </div>
102
+
103
+ {cp.values && Array.isArray(cp.values.messages) && cp.values.messages.length > 0 && (
104
+ <div className="mt-1 p-2 rounded-[8px] bg-black/20 text-[11px] text-text-3 line-clamp-2 italic">
105
+ Last message: {String((cp.values.messages[cp.values.messages.length - 1] as Record<string, unknown>)?.content ?? 'Empty state')}
106
+ </div>
107
+ )}
108
+ </div>
109
+ ))}
110
+ </div>
111
+ )
112
+ }
@@ -1,6 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import { useCallback, useState, type ReactNode } from 'react'
4
+ import { copyTextToClipboard } from '@/lib/clipboard'
4
5
 
5
6
  function extractText(node: ReactNode): string {
6
7
  if (typeof node === 'string') return node
@@ -29,7 +30,8 @@ export function CodeBlock({ children, className }: Props) {
29
30
  const getText = useCallback(() => extractText(children), [children])
30
31
 
31
32
  const handleCopy = useCallback(() => {
32
- navigator.clipboard.writeText(getText()).then(() => {
33
+ void copyTextToClipboard(getText()).then((copiedText) => {
34
+ if (!copiedText) return
33
35
  setCopied(true)
34
36
  setTimeout(() => setCopied(false), 2000)
35
37
  })