@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
@@ -1,5 +1,6 @@
1
1
  'use client'
2
2
 
3
+ import { useState, useEffect } from 'react'
3
4
  import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
4
5
  import { useApprovalStore } from '@/stores/use-approval-store'
5
6
 
@@ -9,13 +10,19 @@ interface Props {
9
10
 
10
11
  export function ExecApprovalCard({ approval }: Props) {
11
12
  const resolveApproval = useApprovalStore((s) => s.resolveApproval)
13
+ const [now, setNow] = useState(() => Date.now())
14
+
15
+ useEffect(() => {
16
+ const timer = setInterval(() => setNow(Date.now()), 5000)
17
+ return () => clearInterval(timer)
18
+ }, [])
12
19
 
13
20
  const handleResolve = (decision: ExecApprovalDecision) => {
14
21
  resolveApproval(approval.id, decision)
15
22
  }
16
23
 
17
24
  const alreadyResolved = approval.error?.includes('Already resolved') ?? false
18
- const expired = approval.expiresAtMs < Date.now()
25
+ const expired = approval.expiresAtMs < now
19
26
  const disabled = !!approval.resolving || expired || alreadyResolved
20
27
 
21
28
  return (
@@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm'
6
6
  import rehypeHighlight from 'rehype-highlight'
7
7
  import type { Message } from '@/types'
8
8
  import { useAppStore } from '@/stores/use-app-store'
9
+ import { useChatStore } from '@/stores/use-chat-store'
9
10
  import { AiAvatar } from '@/components/shared/avatar'
10
11
  import { AgentAvatar } from '@/components/agents/agent-avatar'
11
12
  import { CodeBlock } from './code-block'
@@ -17,6 +18,7 @@ import { FilePathChip, FILE_PATH_RE, DIR_PATH_RE } from './file-path-chip'
17
18
  import { TransferAgentPicker } from './transfer-agent-picker'
18
19
  import { DelegationBanner, DelegationSourceBanner, TaskCompletionCard, parseTaskCompletion } from './delegation-banner'
19
20
  import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
21
+ import { copyTextToClipboard } from '@/lib/clipboard'
20
22
 
21
23
  /** Parse delegation-source metadata prefix from system messages */
22
24
  const DELEGATION_SOURCE_RE = /^\[delegation-source:([^:]*):([^:]*):([^\]]*)\]/
@@ -151,6 +153,33 @@ interface Props {
151
153
  export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentAvatarUrl, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork, onTransferToAgent, momentOverlay }: Props) {
152
154
  const isUser = message.role === 'user'
153
155
  const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
156
+ const isPluginUI = !isUser && message.kind === 'plugin-ui'
157
+ const scaffoldRequest = useMemo(() => {
158
+ if (isUser) return null
159
+ try {
160
+ const data = JSON.parse(message.text)
161
+ if (data.type === 'plugin_scaffold_request') return data
162
+ } catch { /* ignore */ }
163
+ return null
164
+ }, [message.text, isUser])
165
+
166
+ const installRequest = useMemo(() => {
167
+ if (isUser) return null
168
+ try {
169
+ const data = JSON.parse(message.text)
170
+ if (data.type === 'plugin_install_request') return data
171
+ } catch { /* ignore */ }
172
+ return null
173
+ }, [message.text, isUser])
174
+
175
+ const walletRequest = useMemo(() => {
176
+ if (isUser) return null
177
+ try {
178
+ const data = JSON.parse(message.text)
179
+ if (data.type === 'plugin_wallet_transfer_request') return data
180
+ } catch { /* ignore */ }
181
+ return null
182
+ }, [message.text, isUser])
154
183
  const currentUser = useAppStore((s) => s.currentUser)
155
184
  const [copied, setCopied] = useState(false)
156
185
  const [heartbeatExpanded, setHeartbeatExpanded] = useState(false)
@@ -217,7 +246,8 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
217
246
  const displayText = delegationSource ? delegationSource.rest : message.text
218
247
 
219
248
  const handleCopy = useCallback(() => {
220
- navigator.clipboard.writeText(message.text).then(() => {
249
+ void copyTextToClipboard(message.text).then((copiedText) => {
250
+ if (!copiedText) return
221
251
  setCopied(true)
222
252
  setTimeout(() => setCopied(false), 2000)
223
253
  })
@@ -226,7 +256,6 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
226
256
  return (
227
257
  <div
228
258
  className={`group ${isUser ? 'flex flex-col items-end' : 'flex flex-col items-start relative pl-[44px]'}`}
229
- style={{ animation: `${isUser ? 'msg-in-right' : 'msg-in-left'} 0.35s cubic-bezier(0.16, 1, 0.3, 1)` }}
230
259
  >
231
260
  {/* Avatar on spine (assistant) */}
232
261
  {!isUser && (
@@ -447,7 +476,138 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
447
476
  <div className={`${isStructured ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
448
477
  {renderAttachments(message)}
449
478
 
450
- {isHeartbeat ? (
479
+ {walletRequest ? (
480
+ <div className="flex flex-col gap-3 p-4 rounded-[18px] bg-sky-500/[0.03] border border-sky-500/20 shadow-[0_0_20px_rgba(14,165,233,0.05)]">
481
+ <div className="flex items-center gap-2 mb-1">
482
+ <div className="w-5 h-5 rounded-full bg-sky-500/20 flex items-center justify-center text-sky-400">
483
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
484
+ <path d="M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6" />
485
+ </svg>
486
+ </div>
487
+ <span className="text-[11px] font-700 uppercase tracking-wider text-sky-400/80">Wallet Transfer Request</span>
488
+ </div>
489
+ <p className="text-[13px] text-text-2/90 leading-relaxed">{walletRequest.message}</p>
490
+ <div className="p-3 rounded-[12px] bg-black/40 border border-white/5 flex flex-col gap-2">
491
+ <div className="flex justify-between items-center">
492
+ <span className="text-[11px] text-text-3/60 font-600 uppercase">Amount</span>
493
+ <span className="text-[13px] font-700 text-sky-400">{walletRequest.amountSol} SOL</span>
494
+ </div>
495
+ <div className="flex flex-col gap-1">
496
+ <span className="text-[11px] text-text-3/60 font-600 uppercase">To Address</span>
497
+ <span className="text-[11px] font-mono text-text-2/70 break-all">{walletRequest.toAddress}</span>
498
+ </div>
499
+ {walletRequest.memo && (
500
+ <div className="flex flex-col gap-1 border-t border-white/5 pt-2">
501
+ <span className="text-[11px] text-text-3/60 font-600 uppercase">Memo</span>
502
+ <span className="text-[12px] text-text-3/80 italic">&quot;{walletRequest.memo}&quot;</span>
503
+ </div>
504
+ )}
505
+ </div>
506
+ <div className="flex gap-2 mt-1">
507
+ <button
508
+ onClick={() => useChatStore.getState().sendMessage(`I approve this transfer of ${walletRequest.amountSol} SOL to ${walletRequest.toAddress}. Proceed with wallet_tool and set approved=true.`)}
509
+ className="px-4 py-2 rounded-[12px] bg-sky-500 text-black text-[13px] font-700 hover:bg-sky-400 transition-all active:scale-[0.98]"
510
+ >
511
+ Approve & Send
512
+ </button>
513
+ <button
514
+ onClick={() => useChatStore.getState().sendMessage(`I do not approve this transaction. Cancel it.`)}
515
+ className="px-4 py-2 rounded-[12px] bg-white/[0.05] hover:bg-white/[0.1] text-text-2 text-[13px] font-600 transition-all border border-white/10"
516
+ >
517
+ Reject
518
+ </button>
519
+ </div>
520
+ </div>
521
+ ) : installRequest ? (
522
+ <div className="flex flex-col gap-3 p-4 rounded-[18px] bg-emerald-500/[0.03] border border-emerald-500/20 shadow-[0_0_20px_rgba(16,185,129,0.05)]">
523
+ <div className="flex items-center gap-2 mb-1">
524
+ <div className="w-5 h-5 rounded-full bg-emerald-500/20 flex items-center justify-center text-emerald-400">
525
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
526
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
527
+ <polyline points="17 8 12 3 7 8" />
528
+ <line x1="12" y1="3" x2="12" y2="15" />
529
+ </svg>
530
+ </div>
531
+ <span className="text-[11px] font-700 uppercase tracking-wider text-emerald-400/80">Plugin Install Request</span>
532
+ </div>
533
+ <p className="text-[13px] text-text-2/90 leading-relaxed">{installRequest.message}</p>
534
+ <div className="p-3 rounded-[12px] bg-black/40 border border-white/5 flex flex-col gap-1">
535
+ <div className="text-[11px] text-text-3/60 font-600 uppercase tracking-tight">Source URL</div>
536
+ <div className="text-[12px] font-mono text-emerald-200/70 truncate">{installRequest.url}</div>
537
+ </div>
538
+ <div className="flex gap-2 mt-1">
539
+ <button
540
+ onClick={() => useChatStore.getState().sendMessage(`I approve the installation of the plugin from ${installRequest.url}. Proceed with install_request and set approved=true.`)}
541
+ className="px-4 py-2 rounded-[12px] bg-emerald-500 text-black text-[13px] font-700 hover:bg-emerald-400 transition-all active:scale-[0.98]"
542
+ >
543
+ Approve & Install
544
+ </button>
545
+ <button
546
+ onClick={() => useChatStore.getState().sendMessage(`I do not approve this plugin installation. Please stop.`)}
547
+ className="px-4 py-2 rounded-[12px] bg-white/[0.05] hover:bg-white/[0.1] text-text-2 text-[13px] font-600 transition-all border border-white/10"
548
+ >
549
+ Reject
550
+ </button>
551
+ </div>
552
+ </div>
553
+ ) : scaffoldRequest ? (
554
+ <div className="flex flex-col gap-3 p-4 rounded-[18px] bg-amber-500/[0.03] border border-amber-500/20 shadow-[0_0_20px_rgba(245,158,11,0.05)]">
555
+ <div className="flex items-center gap-2 mb-1">
556
+ <div className="w-5 h-5 rounded-full bg-amber-500/20 flex items-center justify-center text-amber-400">
557
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
558
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
559
+ </svg>
560
+ </div>
561
+ <span className="text-[11px] font-700 uppercase tracking-wider text-amber-400/80">Plugin Creation Request</span>
562
+ </div>
563
+ <p className="text-[13px] text-text-2/90 leading-relaxed">{scaffoldRequest.message}</p>
564
+ <div className="p-3 rounded-[12px] bg-black/40 border border-white/5">
565
+ <div className="text-[11px] font-mono text-text-3/60 mb-2 border-b border-white/5 pb-1">filename: {scaffoldRequest.filename}</div>
566
+ <div className="text-[12px] font-mono text-amber-200/70 max-h-[200px] overflow-y-auto whitespace-pre-wrap break-all">
567
+ {scaffoldRequest.code}
568
+ </div>
569
+ </div>
570
+ <div className="flex gap-2 mt-1">
571
+ <button
572
+ onClick={() => useChatStore.getState().sendMessage(`I approve the creation of the plugin ${scaffoldRequest.filename}. Proceed with scaffold and set approved=true.`)}
573
+ className="px-4 py-2 rounded-[12px] bg-amber-500 text-black text-[13px] font-700 hover:bg-amber-400 transition-all active:scale-[0.98]"
574
+ >
575
+ Approve & Install
576
+ </button>
577
+ <button
578
+ onClick={() => useChatStore.getState().sendMessage(`I do not approve the creation of this plugin. Please find another way or stop.`)}
579
+ className="px-4 py-2 rounded-[12px] bg-white/[0.05] hover:bg-white/[0.1] text-text-2 text-[13px] font-600 transition-all border border-white/10"
580
+ >
581
+ Reject
582
+ </button>
583
+ </div>
584
+ </div>
585
+ ) : isPluginUI ? (
586
+ <div className="flex flex-col gap-2 p-4 rounded-[18px] bg-emerald-500/[0.03] border border-emerald-500/10 shadow-[0_0_20px_rgba(16,185,129,0.05)]">
587
+ <div className="flex items-center gap-2 mb-2">
588
+ <div className="w-5 h-5 rounded-full bg-emerald-500/20 flex items-center justify-center">
589
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#10B981" strokeWidth="2.5">
590
+ <path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
591
+ </svg>
592
+ </div>
593
+ <span className="text-[11px] font-700 uppercase tracking-wider text-emerald-400/80">Plugin UI Extension</span>
594
+ </div>
595
+ <div className="text-[14px] text-text-2/90 leading-relaxed">
596
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.text}</ReactMarkdown>
597
+ </div>
598
+ <div className="flex gap-2 mt-2">
599
+ {tryParseJson(message.text)?.actions ? (tryParseJson(message.text)!.actions as Array<{ id: string; href: string; label: string }>).map((action) => (
600
+ <button
601
+ key={action.id}
602
+ onClick={() => window.open(action.href, '_blank')}
603
+ className="px-3 py-1.5 rounded-[10px] bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-400 text-[11px] font-600 transition-all border border-emerald-500/10"
604
+ >
605
+ {action.label}
606
+ </button>
607
+ )) : null}
608
+ </div>
609
+ </div>
610
+ ) : isHeartbeat ? (
451
611
  <div className="flex flex-col gap-2">
452
612
  <button
453
613
  type="button"
@@ -660,7 +820,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
660
820
  )}
661
821
 
662
822
  {/* Action buttons */}
663
- <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 ${isUser ? 'justify-end' : ''}`}>
823
+ <div className={`flex items-center gap-1 mt-1.5 px-1 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-all duration-200 translate-y-2 md:group-hover:translate-y-0 ${isUser ? 'justify-end' : ''}`}>
664
824
  <button
665
825
  onClick={handleCopy}
666
826
  aria-label="Copy message"
@@ -11,6 +11,8 @@ import { StreamingBubble } from './streaming-bubble'
11
11
  import { ThinkingIndicator } from './thinking-indicator'
12
12
  import { SuggestionsBar } from './suggestions-bar'
13
13
  import { ExecApprovalCard } from './exec-approval-card'
14
+ import { TaskApprovalCard } from './task-approval-card'
15
+ import { SessionApprovalCard } from './session-approval-card'
14
16
  import { HeartbeatMoment, ActivityMoment, isNotableTool } from './activity-moment'
15
17
  import { useApprovalStore } from '@/stores/use-approval-store'
16
18
  import { useWs } from '@/hooks/use-ws'
@@ -511,7 +513,14 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
511
513
  }
512
514
 
513
515
  return (
514
- <div key={`${msg.time}-${i}`} data-message-index={i}>
516
+ <div
517
+ key={`${sessionId}-${msg.time}-${i}`}
518
+ data-message-index={i}
519
+ style={{
520
+ animation: `${msg.role === 'user' ? 'msg-in-right' : 'msg-in-left'} 0.4s var(--ease-spring) both`,
521
+ animationDelay: `${Math.min(i * 0.05, 0.4)}s`
522
+ }}
523
+ >
515
524
  {showDateSep && (
516
525
  <div className="flex items-center gap-4 py-2 mb-2">
517
526
  <div className="flex-1 h-px bg-white/[0.06]" />
@@ -573,13 +582,46 @@ export function MessageList({ messages, streaming, connectorFilter = null }: Pro
573
582
 
574
583
  function ApprovalCards({ agentId }: { agentId?: string | null }) {
575
584
  const approvals = useApprovalStore((s) => s.approvals)
585
+ const tasks = useAppStore((s) => s.tasks)
586
+ const sessionId = useAppStore((s) => s.currentSessionId)
587
+ const serverApprovals = useAppStore((s) => s.approvals)
588
+
576
589
  const cards = Object.values(approvals).filter((a) => !agentId || a.agentId === agentId)
577
- if (!cards.length) return null
590
+
591
+ // Find tasks associated with this session that need approval
592
+ const pendingTasks = Object.values(tasks).filter((t) => {
593
+ if (!t.pendingApproval) return false
594
+ return t.sessionId === sessionId || (agentId && t.agentId === agentId)
595
+ })
596
+
597
+ // Server-side approvals (tool_access, wallet, plugin) matching this session/agent
598
+ // Exclude any that overlap with task-based approvals to prevent duplicates
599
+ const pendingTaskIds = new Set(pendingTasks.map((t) => t.id))
600
+ const sessionApprovalCards = Object.values(serverApprovals).filter((a) => {
601
+ if (a.status !== 'pending') return false
602
+ if (a.category === 'task_tool') return false // Already shown via TaskApprovalCard
603
+ if (a.category === 'tool_access') return false // Handled inline by ToolRequestBanner (with auto-continue)
604
+ if (a.taskId && pendingTaskIds.has(a.taskId)) return false // Dedupe with task card
605
+ return a.sessionId === sessionId || (agentId && a.agentId === agentId)
606
+ })
607
+
608
+ if (!cards.length && !pendingTasks.length && !sessionApprovalCards.length) return null
609
+
610
+ const handleSessionApprovalResolved = () => {
611
+ useAppStore.getState().loadApprovals()
612
+ }
613
+
578
614
  return (
579
- <>
615
+ <div className="flex flex-col gap-2">
580
616
  {cards.map((a) => (
581
617
  <ExecApprovalCard key={a.id} approval={a} />
582
618
  ))}
583
- </>
619
+ {pendingTasks.map((t) => (
620
+ <TaskApprovalCard key={t.id} task={t} />
621
+ ))}
622
+ {sessionApprovalCards.map((a) => (
623
+ <SessionApprovalCard key={a.id} approval={a} onResolved={handleSessionApprovalResolved} />
624
+ ))}
625
+ </div>
584
626
  )
585
627
  }
@@ -0,0 +1,80 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { ApprovalRequest } from '@/types'
5
+ import { api } from '@/lib/api-client'
6
+ import { useAppStore } from '@/stores/use-app-store'
7
+ import { toast } from 'sonner'
8
+ import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
9
+
10
+ interface Props {
11
+ approval: ApprovalRequest
12
+ onResolved?: () => void
13
+ }
14
+
15
+ export function SessionApprovalCard({ approval, onResolved }: Props) {
16
+ const [resolving, setResolving] = useState(false)
17
+ const loadApprovals = useAppStore((s) => s.loadApprovals)
18
+ const loadSessions = useAppStore((s) => s.loadSessions)
19
+
20
+ const handleResolve = async (approved: boolean) => {
21
+ setResolving(true)
22
+ try {
23
+ await api('POST', '/approvals', { id: approval.id, approved })
24
+ toast.success(approved ? 'Action approved' : 'Action rejected')
25
+ await Promise.all([loadApprovals(), loadSessions()])
26
+ onResolved?.()
27
+ } catch (err: unknown) {
28
+ toast.error(err instanceof Error ? err.message : 'Failed to submit decision')
29
+ } finally {
30
+ setResolving(false)
31
+ }
32
+ }
33
+
34
+ const payload = getApprovalPayload(approval)
35
+ const title = getApprovalTitle(approval)
36
+
37
+ return (
38
+ <div className="my-2 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] p-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
39
+ <div className="flex items-center gap-2 mb-3">
40
+ <div className="w-5 h-5 rounded-full bg-amber-500/20 flex items-center justify-center">
41
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-amber-400">
42
+ <path d="M12 9v2m0 4h.01" />
43
+ <path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
44
+ </svg>
45
+ </div>
46
+ <span className="text-[12px] font-700 text-amber-400 uppercase tracking-wider">Approval Required</span>
47
+ </div>
48
+
49
+ <p className="text-[13px] text-text-2 mb-2 font-600">{title}</p>
50
+ {approval.description && (
51
+ <p className="text-[12px] text-text-3/90 mb-3">{approval.description}</p>
52
+ )}
53
+
54
+ <div className="bg-black/30 rounded-[10px] border border-white/[0.04] p-3 mb-4 overflow-x-auto max-h-[200px] overflow-y-auto">
55
+ <pre className="text-[11px] font-mono text-text-2/80 leading-relaxed whitespace-pre-wrap break-all">
56
+ {JSON.stringify(payload, null, 2)}
57
+ </pre>
58
+ </div>
59
+
60
+ <div className="flex items-center gap-2">
61
+ <button
62
+ onClick={() => handleResolve(true)}
63
+ disabled={resolving}
64
+ className="flex-1 px-4 py-2 rounded-[10px] bg-emerald-500 text-[#000] text-[12px] font-700 hover:brightness-110 active:scale-[0.98] transition-all disabled:opacity-50"
65
+ style={{ fontFamily: 'inherit' }}
66
+ >
67
+ {resolving ? 'Applying...' : 'Approve'}
68
+ </button>
69
+ <button
70
+ onClick={() => handleResolve(false)}
71
+ disabled={resolving}
72
+ className="px-4 py-2 rounded-[10px] bg-white/[0.04] border border-white/[0.08] text-text-3 text-[12px] font-600 hover:bg-white/[0.08] active:scale-[0.98] transition-all disabled:opacity-50"
73
+ style={{ fontFamily: 'inherit' }}
74
+ >
75
+ Reject
76
+ </button>
77
+ </div>
78
+ </div>
79
+ )
80
+ }
@@ -3,6 +3,8 @@
3
3
  import { useState, useEffect } from 'react'
4
4
  import type { Message } from '@/types'
5
5
  import { IconButton } from '@/components/shared/icon-button'
6
+ import { CheckpointTimeline } from './checkpoint-timeline'
7
+ import { useAppStore } from '@/stores/use-app-store'
6
8
 
7
9
  interface Props {
8
10
  messages: Message[]
@@ -60,23 +62,16 @@ const TYPE_COLORS: Record<EventType, string> = {
60
62
  tool_call: '#8B5CF6',
61
63
  }
62
64
 
63
- const TYPE_ICONS: Record<EventType, string> = {
64
- user: 'U',
65
- assistant: 'AI',
66
- delegation: 'D',
67
- agent_result: 'R',
68
- system: 'S',
69
- error: '!',
70
- tool_call: 'T',
71
- }
72
-
73
65
  function fmtTime(ts: number) {
74
66
  return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
75
67
  }
76
68
 
77
69
  export function SessionDebugPanel({ messages, open, onClose }: Props) {
70
+ const [tab, setTab] = useState<'log' | 'timeline'>('log')
78
71
  const [filter, setFilter] = useState<EventType | 'all'>('all')
79
72
  const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
73
+
74
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
80
75
 
81
76
  const events = messages.map(classifyMessage)
82
77
  const filtered = filter === 'all' ? events : events.filter((e) => e.type === filter)
@@ -105,8 +100,23 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
105
100
  <path d="M18 20V4" />
106
101
  <path d="M6 20v-4" />
107
102
  </svg>
108
- <span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session Debug</span>
109
- <span className="text-[12px] text-text-3 font-mono">{events.length} events</span>
103
+ <span className="font-display text-[16px] font-600 tracking-[-0.02em] flex-1">Session X-Ray</span>
104
+
105
+ <div className="flex bg-white/[0.04] p-0.5 rounded-[8px] mr-2">
106
+ <button
107
+ onClick={() => setTab('log')}
108
+ className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'log' ? 'bg-white/[0.08] text-text shadow-sm' : 'text-text-3 hover:text-text-2'}`}
109
+ >
110
+ Event Log
111
+ </button>
112
+ <button
113
+ onClick={() => setTab('timeline')}
114
+ className={`px-3 py-1 rounded-[6px] text-[11px] font-600 transition-all ${tab === 'timeline' ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:text-text-2'}`}
115
+ >
116
+ Time Travel
117
+ </button>
118
+ </div>
119
+
110
120
  <IconButton onClick={onClose} aria-label="Close debug panel">
111
121
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
112
122
  <line x1="18" y1="6" x2="6" y2="18" />
@@ -115,82 +125,94 @@ export function SessionDebugPanel({ messages, open, onClose }: Props) {
115
125
  </IconButton>
116
126
  </div>
117
127
 
118
- {/* Filters */}
119
- <div className="flex gap-2 px-5 py-3 border-b border-white/[0.04] overflow-x-auto shrink-0">
120
- {filters.map((f) => (
121
- <button
122
- key={f.id}
123
- onClick={() => setFilter(f.id)}
124
- className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border whitespace-nowrap
125
- ${filter === f.id
126
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
127
- : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
128
- style={{ fontFamily: 'inherit' }}
129
- >
130
- {f.label}
131
- </button>
132
- ))}
133
- </div>
134
-
135
- {/* Event timeline */}
136
- <div className="flex-1 overflow-y-auto px-5 py-4">
137
- <div className="relative">
138
- {/* Timeline line */}
139
- <div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06]" />
140
-
141
- {filtered.map((event, i) => {
142
- const color = TYPE_COLORS[event.type]
143
- const expanded = expandedIdx === i
144
- return (
128
+ {tab === 'log' ? (
129
+ <>
130
+ {/* Filters */}
131
+ <div className="flex gap-2 px-5 py-3 border-b border-white/[0.04] overflow-x-auto shrink-0">
132
+ {filters.map((f) => (
145
133
  <button
146
- key={i}
147
- onClick={() => setExpandedIdx(expanded ? null : i)}
148
- className="w-full text-left relative pl-10 pb-4 group cursor-pointer"
134
+ key={f.id}
135
+ onClick={() => setFilter(f.id)}
136
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border whitespace-nowrap
137
+ ${filter === f.id
138
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
139
+ : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
140
+ style={{ fontFamily: 'inherit' }}
149
141
  >
150
- {/* Dot */}
151
- <div
152
- className="absolute left-[10px] top-1 w-[11px] h-[11px] rounded-full border-2"
153
- style={{ borderColor: color, backgroundColor: expanded ? color : 'transparent' }}
154
- />
155
-
156
- {/* Content */}
157
- <div className="flex items-center gap-2 mb-0.5">
158
- <span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
159
- {event.label}
160
- </span>
161
- <span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
162
- </div>
163
-
164
- <p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
165
- {event.detail}
166
- </p>
167
-
168
- {!expanded && event.detail.length > 150 && (
169
- <span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
170
- )}
142
+ {f.label}
171
143
  </button>
172
- )
173
- })}
174
-
175
- {filtered.length === 0 && (
176
- <p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
177
- )}
144
+ ))}
145
+ </div>
146
+
147
+ {/* Event timeline */}
148
+ <div className="flex-1 overflow-y-auto px-5 py-4">
149
+ <div className="relative">
150
+ {/* Timeline line */}
151
+ <div className="absolute left-[15px] top-0 bottom-0 w-px bg-white/[0.06]" />
152
+
153
+ {filtered.map((event, i) => {
154
+ const color = TYPE_COLORS[event.type]
155
+ const expanded = expandedIdx === i
156
+ return (
157
+ <button
158
+ key={i}
159
+ onClick={() => setExpandedIdx(expanded ? null : i)}
160
+ className="w-full text-left relative pl-10 pb-4 group cursor-pointer"
161
+ >
162
+ {/* Dot */}
163
+ <div
164
+ className="absolute left-[10px] top-1 w-[11px] h-[11px] rounded-full border-2"
165
+ style={{ borderColor: color, backgroundColor: expanded ? color : 'transparent' }}
166
+ />
167
+
168
+ {/* Content */}
169
+ <div className="flex items-center gap-2 mb-0.5">
170
+ <span className="text-[11px] font-700 uppercase tracking-wider" style={{ color }}>
171
+ {event.label}
172
+ </span>
173
+ <span className="text-[10px] text-text-3/70 font-mono">{fmtTime(event.time)}</span>
174
+ </div>
175
+
176
+ <p className={`text-[12px] text-text-3 leading-[1.5] ${expanded ? 'whitespace-pre-wrap' : 'line-clamp-2'}`}>
177
+ {event.detail}
178
+ </p>
179
+
180
+ {!expanded && event.detail.length > 150 && (
181
+ <span className="text-[11px] text-accent-bright/60 mt-1 inline-block">click to expand</span>
182
+ )}
183
+ </button>
184
+ )
185
+ })}
186
+
187
+ {filtered.length === 0 && (
188
+ <p className="text-center text-[13px] text-text-3 py-12">No events matching filter</p>
189
+ )}
190
+ </div>
191
+ </div>
192
+
193
+ {/* Stats bar */}
194
+ <div className="flex items-center gap-4 px-5 py-3 border-t border-white/[0.06] shrink-0">
195
+ {(['delegation', 'agent_result', 'error'] as EventType[]).map((type) => {
196
+ const count = events.filter((e) => e.type === type).length
197
+ if (!count) return null
198
+ return (
199
+ <span key={type} className="flex items-center gap-1.5 text-[11px] font-mono" style={{ color: TYPE_COLORS[type] }}>
200
+ <span className="w-2 h-2 rounded-full" style={{ backgroundColor: TYPE_COLORS[type] }} />
201
+ {count} {type === 'delegation' ? 'delegations' : type === 'agent_result' ? 'results' : 'errors'}
202
+ </span>
203
+ )
204
+ })}
205
+ </div>
206
+ </>
207
+ ) : (
208
+ <div className="flex-1 overflow-y-auto">
209
+ {currentSessionId ? (
210
+ <CheckpointTimeline sessionId={currentSessionId} />
211
+ ) : (
212
+ <div className="p-12 text-center text-text-3">No active session</div>
213
+ )}
178
214
  </div>
179
- </div>
180
-
181
- {/* Stats bar */}
182
- <div className="flex items-center gap-4 px-5 py-3 border-t border-white/[0.06] shrink-0">
183
- {(['delegation', 'agent_result', 'error'] as EventType[]).map((type) => {
184
- const count = events.filter((e) => e.type === type).length
185
- if (!count) return null
186
- return (
187
- <span key={type} className="flex items-center gap-1.5 text-[11px] font-mono" style={{ color: TYPE_COLORS[type] }}>
188
- <span className="w-2 h-2 rounded-full" style={{ backgroundColor: TYPE_COLORS[type] }} />
189
- {count} {type === 'delegation' ? 'delegations' : type === 'agent_result' ? 'results' : 'errors'}
190
- </span>
191
- )
192
- })}
193
- </div>
215
+ )}
194
216
  </div>
195
217
  )
196
218
  }
@@ -135,7 +135,7 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAva
135
135
  return (
136
136
  <div
137
137
  className="flex flex-col items-start relative pl-[44px]"
138
- style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
138
+ style={{ animation: 'msg-in-left 0.4s var(--ease-spring) both' }}
139
139
  >
140
140
  <div className="absolute left-[4px] top-0 relative">
141
141
  {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
@@ -160,13 +160,14 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentAva
160
160
  {text && thinkingText && (
161
161
  <div className="max-w-[85%] md:max-w-[72%] mb-2">
162
162
  <details className="group rounded-[12px] border border-purple-500/15 bg-purple-500/[0.04]">
163
- <summary className="flex items-center gap-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
164
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-purple-400/60 shrink-0 transition-transform group-open:rotate-90">
163
+ <summary className="flex items-center gap-2 px-3.5 py-2.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden relative overflow-hidden group-open:rounded-b-none">
164
+ <div className="absolute inset-0 bg-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity" style={{ animation: 'pulse-subtle 3s ease-in-out infinite' }} />
165
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-purple-400/60 shrink-0 transition-transform group-open:rotate-90 relative z-10">
165
166
  <polyline points="9 18 15 12 9 6" />
166
167
  </svg>
167
- <span className="text-[11px] font-600 text-purple-400/70 uppercase tracking-[0.05em]">Thinking</span>
168
+ <span className="text-[11px] font-600 text-purple-400/70 uppercase tracking-[0.05em] relative z-10">Thinking</span>
168
169
  </summary>
169
- <div className="px-3.5 pb-3 pt-1 max-h-[300px] overflow-y-auto">
170
+ <div className="px-3.5 pb-3 pt-1 max-h-[300px] overflow-y-auto border-t border-white/[0.04]">
170
171
  <div className="text-[13px] leading-[1.6] text-text-3/70 whitespace-pre-wrap break-words">
171
172
  {thinkingText}
172
173
  </div>