@swarmclawai/swarmclaw 0.6.8 → 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 (166) hide show
  1. package/README.md +70 -45
  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 +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -40,31 +40,64 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
40
40
  const streamToolName = useChatStore((s) => s.streamToolName)
41
41
  const thinkingText = useChatStore((s) => s.thinkingText)
42
42
  const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
43
+ const agentStatus = useChatStore((s) => s.agentStatus)
43
44
 
44
45
  const statusText = streamPhase === 'tool' && streamToolName
45
46
  ? `Using ${streamToolName}...`
46
47
  : 'Thinking...'
47
48
 
48
49
  const hasThinkingContent = thinkingText.trim().length > 0
50
+ const hasMission = !!agentStatus?.goal
49
51
 
50
52
  return (
51
53
  <div className="flex flex-col items-start relative pl-[44px]"
52
- style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
54
+ style={{ animation: 'msg-in-left 0.4s var(--ease-spring) both' }}>
53
55
  <div className="absolute left-[4px] top-0">
54
56
  {agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
55
57
  </div>
58
+
56
59
  <div className="flex items-center gap-2.5 mb-2 px-1">
57
60
  <span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
61
+ {agentStatus?.status && (
62
+ <span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-700 uppercase tracking-wider ${
63
+ agentStatus.status === 'progress' ? 'bg-blue-500/10 text-blue-400' :
64
+ agentStatus.status === 'ok' ? 'bg-emerald-500/10 text-emerald-400' :
65
+ agentStatus.status === 'blocked' ? 'bg-red-500/10 text-red-400' :
66
+ 'bg-white/[0.06] text-text-3'
67
+ }`} style={{ animation: 'spring-in 0.3s var(--ease-spring)' }}>
68
+ {agentStatus.status}
69
+ </span>
70
+ )}
58
71
  </div>
59
72
 
73
+ {hasMission && (
74
+ <div className="mb-2 w-full max-w-[85%] md:max-w-[72%] p-3 rounded-[12px] border border-accent-bright/10 bg-accent-bright/[0.02]"
75
+ style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
76
+ <div className="text-[10px] font-700 text-accent-bright/60 uppercase tracking-widest mb-1.5 flex items-center gap-2">
77
+ <span className="w-1 h-1 rounded-full bg-accent-bright/40" />
78
+ Active Mission
79
+ </div>
80
+ <p className="text-[13px] font-500 text-text-2 leading-snug">{agentStatus.goal}</p>
81
+ {agentStatus.nextAction && (
82
+ <div className="mt-2 pt-2 border-t border-white/[0.04]">
83
+ <span className="text-[10px] font-600 text-text-3/40 uppercase block mb-0.5">Next Action</span>
84
+ <p className="text-[11px] text-text-3/80 italic">&ldquo;{agentStatus.nextAction}&rdquo;</p>
85
+ </div>
86
+ )}
87
+ </div>
88
+ )}
89
+
60
90
  {hasThinkingContent ? (
61
91
  <details className="group/think w-full max-w-[85%] md:max-w-[72%]">
62
- <summary className="bubble-ai px-5 py-3.5 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
63
- <div className="flex items-center gap-3">
92
+ <summary className="bubble-ai px-5 py-3.5 cursor-pointer list-none [&::-webkit-details-marker]:hidden relative overflow-hidden group-open/think:rounded-b-none border border-transparent hover:border-white/[0.04] transition-all">
93
+ {/* Thinking pulse background */}
94
+ <div className="absolute inset-0 bg-accent-bright/5 opacity-0 group-hover/think:opacity-100 transition-opacity" style={{ animation: 'pulse-subtle 2s ease-in-out infinite' }} />
95
+
96
+ <div className="flex items-center gap-3 relative z-10">
64
97
  <div className="flex gap-2">
65
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
66
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
67
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
98
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
99
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
100
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
68
101
  </div>
69
102
  <span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
70
103
  <ElapsedTimer startTime={thinkingStartTime} />
@@ -77,7 +110,7 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
77
110
  </svg>
78
111
  </div>
79
112
  </summary>
80
- <div className="mt-2 px-4 py-3 rounded-[12px] bg-bg/60 border border-white/[0.04] max-h-[300px] overflow-y-auto">
113
+ <div className="px-4 py-3 rounded-b-[12px] bg-bg/60 border-x border-b border-white/[0.04] max-h-[300px] overflow-y-auto">
81
114
  <div className="msg-content text-[13px] leading-[1.6] text-text-3/80">
82
115
  <ReactMarkdown remarkPlugins={[remarkGfm]}>
83
116
  {thinkingText}
@@ -86,12 +119,15 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
86
119
  </div>
87
120
  </details>
88
121
  ) : (
89
- <div className="bubble-ai px-6 py-5">
90
- <div className="flex items-center gap-3">
122
+ <div className="bubble-ai px-6 py-5 relative overflow-hidden">
123
+ {/* Thinking glow effect */}
124
+ <div className="absolute inset-0 bg-gradient-to-r from-transparent via-accent-bright/5 to-transparent" style={{ animation: 'shimmer-bar 3s linear infinite' }} />
125
+
126
+ <div className="flex items-center gap-3 relative z-10">
91
127
  <div className="flex gap-2">
92
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
93
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
94
- <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
128
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
129
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
130
+ <span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
95
131
  </div>
96
132
  <span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
97
133
  <ElapsedTimer startTime={thinkingStartTime} />
@@ -15,22 +15,36 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
15
15
  const loadSessions = useAppStore((s) => s.loadSessions)
16
16
  const currentSessionId = useAppStore((s) => s.currentSessionId)
17
17
  const sessions = useAppStore((s) => s.sessions)
18
+ const serverApprovals = useAppStore((s) => s.approvals)
19
+ const loadApprovals = useAppStore((s) => s.loadApprovals)
18
20
  const [granted, setGranted] = useState<Set<string>>(new Set())
19
21
  const [denied, setDenied] = useState<Set<string>>(new Set())
20
22
  const continueSentRef = useRef(false)
21
23
 
22
- const toolRequests: { toolId: string; reason: string }[] = []
24
+ // Resolve matching server-side tool_access approval when user grants/denies inline
25
+ const resolveMatchingApproval = (toolId: string, approved: boolean) => {
26
+ const match = Object.values(serverApprovals).find(
27
+ (a) => a.status === 'pending' && a.category === 'tool_access'
28
+ && (a.data?.toolId === toolId || a.data?.pluginId === toolId)
29
+ )
30
+ if (match) {
31
+ api('POST', '/approvals', { id: match.id, approved }).then(() => loadApprovals()).catch(() => { /* best effort */ })
32
+ }
33
+ }
34
+
35
+ const pluginRequests: { pluginId: string; reason: string }[] = []
23
36
  const seen = new Set<string>()
24
37
 
25
38
  function extractFromText(t: string) {
26
39
  try {
27
- const jsonMatches = t.match(/\{"type"\s*:\s*"tool_request"[^}]*\}/g)
40
+ const jsonMatches = t.match(/\{"type"\s*:\s*"(?:tool_request|plugin_request)"[^}]*\}/g)
28
41
  if (jsonMatches) {
29
42
  for (const jm of jsonMatches) {
30
43
  const parsed = JSON.parse(jm)
31
- if (parsed.type === 'tool_request' && parsed.toolId && !seen.has(parsed.toolId)) {
32
- seen.add(parsed.toolId)
33
- toolRequests.push({ toolId: parsed.toolId, reason: parsed.reason || '' })
44
+ const pluginId = parsed.pluginId || parsed.toolId
45
+ if ((parsed.type === 'tool_request' || parsed.type === 'plugin_request') && pluginId && !seen.has(pluginId)) {
46
+ seen.add(pluginId)
47
+ pluginRequests.push({ pluginId, reason: parsed.reason || '' })
34
48
  }
35
49
  }
36
50
  }
@@ -41,7 +55,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
41
55
  extractFromText(text)
42
56
  for (const output of toolOutputs) extractFromText(output)
43
57
 
44
- if (toolRequests.length === 0) return null
58
+ if (pluginRequests.length === 0) return null
45
59
 
46
60
  const sid = currentSessionId
47
61
  const session = sid ? sessions[sid] : null
@@ -59,17 +73,20 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
59
73
  const newGranted = new Set(granted).add(toolId)
60
74
  setGranted(newGranted)
61
75
 
62
- // Auto-continue: once all requested tools are granted, send a follow-up message
63
- const allGranted = toolRequests.every(
64
- (r) => newGranted.has(r.toolId) || updated.includes(r.toolId),
76
+ // Resolve matching server-side approval so approvals page stays in sync
77
+ resolveMatchingApproval(toolId, true)
78
+
79
+ // Notify agent that access was granted with a precise message (not a vague "Continue")
80
+ const allGranted = pluginRequests.every(
81
+ (r) => newGranted.has(r.pluginId) || updated.includes(r.pluginId),
65
82
  )
66
83
  if (allGranted && !continueSentRef.current) {
67
84
  continueSentRef.current = true
68
- // Small delay to let the session update propagate
85
+ const grantedNames = pluginRequests.map((r) => TOOL_LABELS[r.pluginId] || r.pluginId).join(', ')
69
86
  setTimeout(() => {
70
87
  const { streaming, sendMessage } = useChatStore.getState()
71
88
  if (!streaming) {
72
- sendMessage('Continue')
89
+ sendMessage(`Access granted for: ${grantedNames}. You now have these tools available — proceed with your task.`)
73
90
  }
74
91
  }, 300)
75
92
  }
@@ -77,24 +94,26 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
77
94
 
78
95
  const handleDeny = (toolId: string) => {
79
96
  setDenied((prev) => new Set(prev).add(toolId))
97
+ // Resolve matching server-side approval
98
+ resolveMatchingApproval(toolId, false)
80
99
  const label = TOOL_LABELS[toolId] || toolId
81
100
  setTimeout(() => {
82
101
  const { streaming, sendMessage } = useChatStore.getState()
83
102
  if (!streaming) {
84
- sendMessage(`Tool access denied for ${label} — proceed without it.`)
103
+ sendMessage(`Plugin access denied for ${label} — proceed without it.`)
85
104
  }
86
105
  }, 200)
87
106
  }
88
107
 
89
108
  return (
90
109
  <div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mt-2">
91
- {toolRequests.map(({ toolId, reason }) => {
92
- const isGranted = granted.has(toolId) || (session?.tools || []).includes(toolId)
93
- const isDenied = denied.has(toolId)
94
- const label = TOOL_LABELS[toolId] || toolId
110
+ {pluginRequests.map(({ pluginId, reason }) => {
111
+ const isGranted = granted.has(pluginId) || (session?.tools || []).includes(pluginId)
112
+ const isDenied = denied.has(pluginId)
113
+ const label = TOOL_LABELS[pluginId] || pluginId
95
114
  return (
96
115
  <div
97
- key={toolId}
116
+ key={pluginId}
98
117
  className="flex items-center gap-3 px-4 py-3 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.06]"
99
118
  style={{ animation: 'fade-in 0.2s ease' }}
100
119
  >
@@ -103,7 +122,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
103
122
  </svg>
104
123
  <div className="flex-1 min-w-0">
105
124
  <p className="text-[12px] text-text-2 font-600">
106
- Requesting access to <span className="text-amber-400">{label}</span>
125
+ Requesting plugin access to <span className="text-amber-400">{label}</span>
107
126
  </p>
108
127
  {reason && <p className="text-[11px] text-text-3/60 mt-0.5 truncate">{reason}</p>}
109
128
  </div>
@@ -114,14 +133,14 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
114
133
  ) : (
115
134
  <div className="flex gap-1.5 shrink-0">
116
135
  <button
117
- onClick={() => handleGrant(toolId)}
136
+ onClick={() => handleGrant(pluginId)}
118
137
  className="px-3 py-1.5 rounded-[8px] bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-[11px] font-600 border-none cursor-pointer transition-colors"
119
138
  style={{ fontFamily: 'inherit' }}
120
139
  >
121
140
  Grant
122
141
  </button>
123
142
  <button
124
- onClick={() => handleDeny(toolId)}
143
+ onClick={() => handleDeny(pluginId)}
125
144
  className="px-3 py-1.5 rounded-[8px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[11px] font-600 border-none cursor-pointer transition-colors"
126
145
  style={{ fontFamily: 'inherit' }}
127
146
  >
@@ -63,7 +63,7 @@ export function ChatroomList() {
63
63
  ) : (
64
64
  <div className="p-3 space-y-1">
65
65
  {sorted.length > 2 && (
66
- <div className="flex items-center gap-1 px-1 pb-2">
66
+ <div className="flex items-center gap-1 px-1 pb-2" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
67
67
  {(['all', 'active', 'recent'] as const).map((f) => (
68
68
  <button
69
69
  key={f}
@@ -79,7 +79,7 @@ export function ChatroomList() {
79
79
  ))}
80
80
  </div>
81
81
  )}
82
- {filtered.map((chatroom) => {
82
+ {filtered.map((chatroom, idx) => {
83
83
  const isActive = chatroom.id === currentChatroomId
84
84
  const memberNames = chatroom.agentIds
85
85
  .map((id) => agents[id]?.name)
@@ -91,11 +91,15 @@ export function ChatroomList() {
91
91
  <button
92
92
  key={chatroom.id}
93
93
  onClick={() => setCurrentChatroom(chatroom.id)}
94
- className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent ${
94
+ className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent relative overflow-hidden ${
95
95
  isActive
96
96
  ? 'bg-accent-soft/60'
97
- : 'hover:bg-white/[0.04]'
97
+ : 'hover:bg-white/[0.04] hover:scale-[1.01]'
98
98
  }`}
99
+ style={{
100
+ animation: 'fade-up 0.4s var(--ease-spring) both',
101
+ animationDelay: `${idx * 0.03}s`
102
+ }}
99
103
  >
100
104
  <div className="flex items-center gap-2 mb-0.5">
101
105
  <div className="w-7 h-7 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
@@ -106,6 +110,9 @@ export function ChatroomList() {
106
110
  <span className={`text-[13px] font-600 truncate ${isActive ? 'text-accent-bright' : 'text-text'}`}>
107
111
  {chatroom.name}
108
112
  </span>
113
+ {isActive && (
114
+ <div className="absolute left-0 top-3 bottom-3 w-1 rounded-r-full bg-accent-bright" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }} />
115
+ )}
109
116
  <span className="label-mono ml-auto shrink-0">
110
117
  {chatroom.agentIds.length} agents
111
118
  </span>
@@ -202,13 +202,15 @@ export function ChatroomSheet() {
202
202
  }
203
203
  if (editing) {
204
204
  await updateChatroom(editing.id, payload)
205
- toast.success('Chatroom saved')
205
+ toast.success('Chatroom updated successfully')
206
206
  } else {
207
207
  const chatroom = await createChatroom(payload)
208
208
  setCurrentChatroom(chatroom.id)
209
- toast.success('Chatroom created')
209
+ toast.success('Chatroom created successfully')
210
210
  }
211
211
  setChatroomSheetOpen(false)
212
+ } catch (err: unknown) {
213
+ toast.error(err instanceof Error ? err.message : 'Failed to save chatroom')
212
214
  } finally {
213
215
  setSaving(false)
214
216
  }
@@ -216,11 +218,14 @@ export function ChatroomSheet() {
216
218
 
217
219
  const handleDelete = async () => {
218
220
  if (!editing || saving) return
221
+ if (!confirm(`Delete chatroom "${editing.name}"?`)) return
219
222
  setSaving(true)
220
223
  try {
221
224
  await deleteChatroom(editing.id)
222
225
  toast.success('Chatroom deleted')
223
226
  setChatroomSheetOpen(false)
227
+ } catch (err: unknown) {
228
+ toast.error(err instanceof Error ? err.message : 'Failed to delete chatroom')
224
229
  } finally {
225
230
  setSaving(false)
226
231
  }
@@ -31,6 +31,10 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
31
31
  const [reconnecting, setReconnecting] = useState<string | null>(null)
32
32
  const [loaded, setLoaded] = useState(false)
33
33
  const [error, setError] = useState<string | null>(null)
34
+ const openConnector = useCallback((id: string | null) => {
35
+ setEditingConnectorId(id)
36
+ setConnectorSheetOpen(true)
37
+ }, [setEditingConnectorId, setConnectorSheetOpen])
34
38
 
35
39
  const refresh = useCallback(async () => {
36
40
  await Promise.all([loadConnectors(), loadAgents(), loadChatrooms()])
@@ -95,7 +99,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
95
99
  <div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
96
100
  <p className="text-[13px] text-text-3">No connectors configured yet.</p>
97
101
  <button
98
- onClick={() => { setEditingConnectorId(null); setConnectorSheetOpen(true) }}
102
+ onClick={() => openConnector(null)}
99
103
  className="mt-3 text-[13px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
100
104
  >
101
105
  + Add Connector
@@ -113,7 +117,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
113
117
  {error}
114
118
  </div>
115
119
  )}
116
- {list.map((c) => {
120
+ {list.map((c, idx) => {
117
121
  const agent = c.agentId ? agents[c.agentId] : null
118
122
  const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
119
123
  const isRunning = c.status === 'running'
@@ -121,8 +125,12 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
121
125
  return (
122
126
  <button
123
127
  key={c.id}
124
- onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
128
+ onClick={() => openConnector(c.id)}
125
129
  className="w-full flex items-center gap-3 px-5 py-2.5 hover:bg-white/[0.02] transition-colors cursor-pointer bg-transparent border-none text-left"
130
+ style={{
131
+ animation: 'fade-up 0.4s var(--ease-spring) both',
132
+ animationDelay: `${idx * 0.03}s`
133
+ }}
126
134
  >
127
135
  <ConnectorPlatformIcon platform={c.platform} size={16} />
128
136
  <div className="flex-1 min-w-0">
@@ -133,7 +141,8 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
133
141
  </div>
134
142
  <span className={`shrink-0 w-2 h-2 rounded-full ${
135
143
  isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
136
- }`} />
144
+ }`}
145
+ style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : undefined} />
137
146
  </button>
138
147
  )
139
148
  })}
@@ -150,7 +159,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
150
159
  </div>
151
160
  )}
152
161
  <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
153
- {list.map((c) => {
162
+ {list.map((c, idx) => {
154
163
  const platformLabel = getConnectorPlatformLabel(c.platform)
155
164
  const agent = c.agentId ? agents[c.agentId] : null
156
165
  const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
@@ -164,11 +173,23 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
164
173
  const lastMsg = c.presence?.lastMessageAt
165
174
 
166
175
  return (
167
- <button
176
+ <div
168
177
  key={c.id}
169
- onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
170
- className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] text-left w-full"
171
- style={{ fontFamily: 'inherit' }}
178
+ role="button"
179
+ tabIndex={0}
180
+ onClick={() => openConnector(c.id)}
181
+ onKeyDown={(e) => {
182
+ if (e.key === 'Enter' || e.key === ' ') {
183
+ e.preventDefault()
184
+ openConnector(c.id)
185
+ }
186
+ }}
187
+ className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] hover:scale-[1.01] text-left w-full"
188
+ style={{
189
+ fontFamily: 'inherit',
190
+ animation: 'spring-in 0.5s var(--ease-spring) both',
191
+ animationDelay: `${idx * 0.05}s`
192
+ }}
172
193
  >
173
194
  {/* Header: platform badge + status */}
174
195
  <div className="flex items-center gap-3 mb-3">
@@ -178,7 +199,8 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
178
199
  <span className="text-[14px] font-600 text-text truncate">{c.name}</span>
179
200
  <span className={`shrink-0 w-2 h-2 rounded-full ${
180
201
  isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
181
- }`} />
202
+ }`}
203
+ style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : c.status === 'error' ? { animation: 'ai-shake 0.5s' } : undefined} />
182
204
  </div>
183
205
  <span className="text-[11px] text-text-3 block">
184
206
  {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
@@ -264,7 +286,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
264
286
  )}
265
287
  </div>
266
288
  </div>
267
- </button>
289
+ </div>
268
290
  )
269
291
  })}
270
292
  </div>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useState, useEffect, useCallback } from 'react'
3
+ import { useState, useEffect, useCallback, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { api } from '@/lib/api-client'
@@ -242,6 +242,29 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
242
242
  export function ConnectorSheet() {
243
243
  const open = useAppStore((s) => s.connectorSheetOpen)
244
244
  const setOpen = useAppStore((s) => s.setConnectorSheetOpen)
245
+ // ... (existing state)
246
+ const [dynamicPlatforms, setDynamicPlatforms] = useState<Array<{ id: string; name: string; description?: string }>>([])
247
+
248
+ useEffect(() => {
249
+ if (open) {
250
+ api<Array<{ id: string; name: string; description?: string }>>('GET', '/plugins/ui?type=connectors').then(list => {
251
+ setDynamicPlatforms(list || [])
252
+ }).catch(() => {})
253
+ }
254
+ }, [open])
255
+
256
+ const ALL_PLATFORMS = useMemo(() => {
257
+ const plugins = dynamicPlatforms.map(p => ({
258
+ id: p.id,
259
+ label: p.name,
260
+ color: '#10B981',
261
+ setupSteps: [p.description || 'Follow the plugin instructions for setup.'],
262
+ tokenLabel: 'Plugin Token',
263
+ tokenHelp: 'Secret key required by this plugin connector.',
264
+ configFields: [],
265
+ }))
266
+ return [...PLATFORMS, ...plugins]
267
+ }, [dynamicPlatforms])
245
268
  const editingId = useAppStore((s) => s.editingConnectorId)
246
269
  const setEditingId = useAppStore((s) => s.setEditingConnectorId)
247
270
  const connectors = useAppStore((s) => s.connectors)
@@ -393,7 +416,7 @@ export function ConnectorSheet() {
393
416
  setEditingId(null)
394
417
  }
395
418
 
396
- const platformConfig = PLATFORMS.find((p) => p.id === platform)!
419
+ const platformConfig = ALL_PLATFORMS.find((p) => p.id === platform) || ALL_PLATFORMS[0]
397
420
  const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
398
421
  const credList = Object.values(credentials)
399
422
 
@@ -413,17 +436,17 @@ export function ConnectorSheet() {
413
436
  <div className="mb-8">
414
437
  <SectionLabel>Platform</SectionLabel>
415
438
  <div className="grid grid-cols-2 gap-3">
416
- {PLATFORMS.map((p) => (
439
+ {ALL_PLATFORMS.map((p) => (
417
440
  <button
418
441
  key={p.id}
419
- onClick={() => { setPlatform(p.id); setShowSetup(false) }}
442
+ onClick={() => { setPlatform(p.id as ConnectorPlatform); setShowSetup(false) }}
420
443
  className={`flex items-center gap-3 p-4 rounded-[14px] cursor-pointer transition-all duration-200 border text-left
421
444
  ${platform === p.id
422
445
  ? 'bg-white/[0.04] border-white/[0.15] shadow-[0_0_20px_rgba(255,255,255,0.02)]'
423
446
  : 'bg-transparent border-white/[0.04] hover:border-white/[0.08] hover:bg-white/[0.01]'}`}
424
447
  style={{ fontFamily: 'inherit' }}
425
448
  >
426
- <ConnectorPlatformBadge platform={p.id} size={40} iconSize={18} />
449
+ <ConnectorPlatformBadge platform={p.id as ConnectorPlatform} size={40} iconSize={18} />
427
450
  <div>
428
451
  <div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
429
452
  <div className="text-[11px] text-text-3 mt-0.5">
@@ -439,7 +462,7 @@ export function ConnectorSheet() {
439
462
  {/* Editing: show platform badge */}
440
463
  {editing && (
441
464
  <div className="mb-6 flex items-center gap-3">
442
- <ConnectorPlatformBadge platform={platformConfig.id} size={40} iconSize={18} />
465
+ <ConnectorPlatformBadge platform={platformConfig.id as ConnectorPlatform} size={40} iconSize={18} />
443
466
  <div>
444
467
  <div className="text-[14px] font-600 text-text">{platformConfig.label}</div>
445
468
  <div className="flex items-center gap-2 mt-0.5">
@@ -192,7 +192,7 @@ export function HomeView() {
192
192
  <div className="flex-1 overflow-y-auto">
193
193
  <div className="max-w-[800px] mx-auto px-6 py-10">
194
194
  {/* Header */}
195
- <div className="mb-10">
195
+ <div className="mb-10" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
196
196
  <h1 className="font-display text-[28px] font-700 text-text tracking-[-0.03em]">
197
197
  SwarmClaw
198
198
  </h1>
@@ -203,15 +203,15 @@ export function HomeView() {
203
203
 
204
204
  {/* Quick Stats */}
205
205
  <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
206
- <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" />
207
- <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" />
208
- <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" />
209
- <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" />
206
+ <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" index={0} />
207
+ <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" index={1} />
208
+ <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" index={2} />
209
+ <StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" index={3} />
210
210
  </div>
211
211
 
212
212
  {/* Cost trend sparkline */}
213
213
  {costTrend.length > 1 && (
214
- <div className="mb-10 px-1">
214
+ <div className="mb-10 px-1" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
215
215
  <p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1 flex items-center gap-1.5">
216
216
  7-day cost trend <HintTip text="Daily API spend over the past week — hover for details" />
217
217
  </p>
@@ -247,7 +247,7 @@ export function HomeView() {
247
247
 
248
248
  {/* Notifications banner */}
249
249
  {unreadNotifications.length > 0 && (
250
- <section className="mb-8">
250
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
251
251
  <div className="rounded-[14px] border border-amber-400/20 bg-amber-400/[0.04] overflow-hidden">
252
252
  <div className="flex items-center gap-2 px-4 py-2.5 border-b border-amber-400/10">
253
253
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
@@ -283,7 +283,7 @@ export function HomeView() {
283
283
  )}
284
284
 
285
285
  {/* Connector Status */}
286
- <section className="mb-8">
286
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
287
287
  <SectionHeader label="Connectors" onViewAll={allConnectors.length > 0 ? () => setActiveView('connectors') : undefined} />
288
288
  {allConnectors.length > 0 ? (
289
289
  <div className="flex gap-2 flex-wrap">
@@ -307,7 +307,7 @@ export function HomeView() {
307
307
  </section>
308
308
 
309
309
  {/* Two-column layout: Running Tasks + Upcoming Schedules */}
310
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
310
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.45s both' }}>
311
311
  {/* Running Tasks */}
312
312
  <section>
313
313
  <SectionHeader label="Running Tasks" onViewAll={runningTasks.length > 0 ? () => setActiveView('tasks') : undefined} />
@@ -373,7 +373,7 @@ export function HomeView() {
373
373
  </div>
374
374
 
375
375
  {/* Pinned Agents */}
376
- <section className="mb-8">
376
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.5s both' }}>
377
377
  <SectionHeader label="Pinned Agents" />
378
378
  {pinnedAgents.length > 0 ? (
379
379
  <div className="flex gap-3 overflow-x-auto pb-2">
@@ -438,7 +438,7 @@ export function HomeView() {
438
438
  </section>
439
439
 
440
440
  {/* Recent Chats */}
441
- <section className="mb-8">
441
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.55s both' }}>
442
442
  <SectionHeader label="Recent Chats" />
443
443
  {recentChats.length > 0 ? (
444
444
  <div className="flex flex-col gap-1">
@@ -486,7 +486,7 @@ export function HomeView() {
486
486
 
487
487
  {/* Activity Feed */}
488
488
  {recentActivity.length > 0 && (
489
- <section className="mb-10">
489
+ <section className="mb-10" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.6s both' }}>
490
490
  <SectionHeader label="Recent Activity" />
491
491
  <div className="flex flex-col gap-0.5">
492
492
  {recentActivity.map((entry) => (
@@ -526,9 +526,15 @@ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () =>
526
526
  )
527
527
  }
528
528
 
529
- function StatCard({ label, value, accent, hint }: { label: string; value: string; accent?: boolean; hint?: string }) {
529
+ function StatCard({ label, value, accent, hint, index = 0 }: { label: string; value: string; accent?: boolean; hint?: string; index?: number }) {
530
530
  return (
531
- <div className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
531
+ <div
532
+ className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06] hover:bg-white/[0.05] transition-all hover:scale-[1.02] active:scale-[0.98] cursor-default"
533
+ style={{
534
+ animation: 'spring-in 0.6s var(--ease-spring) both',
535
+ animationDelay: `${0.1 + index * 0.05}s`
536
+ }}
537
+ >
532
538
  <p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1 flex items-center gap-1.5">
533
539
  {label}
534
540
  {hint && <HintTip text={hint} />}
@@ -15,13 +15,14 @@ interface Props {
15
15
  streaming: boolean
16
16
  onSend: (text: string) => void
17
17
  onStop: () => void
18
+ pluginChatActions?: Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>
18
19
  }
19
20
 
20
21
  // FilePreview is now imported from @/components/shared/file-preview
21
22
 
22
23
  const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
23
24
 
24
- export function ChatInput({ streaming, onSend, onStop }: Props) {
25
+ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }: Props) {
25
26
  const [value, setValue] = useState('')
26
27
  const { ref: textareaRef, resize } = useAutoResize()
27
28
  const fileInputRef = useRef<HTMLInputElement>(null)
@@ -219,6 +220,26 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
219
220
  <span className="hidden sm:inline">Image</span>
220
221
  </button>
221
222
 
223
+ {/* Plugin Chat Actions */}
224
+ {pluginChatActions.map((action) => (
225
+ <Tooltip key={action.id}>
226
+ <TooltipTrigger asChild>
227
+ <button
228
+ onClick={() => {
229
+ if (action.action === 'message') onSend(action.value)
230
+ else if (action.action === 'link') window.open(action.value, '_blank')
231
+ }}
232
+ className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-emerald-500/[0.05]
233
+ text-emerald-400 text-[13px] cursor-pointer hover:text-emerald-300 hover:bg-emerald-500/[0.1] transition-all duration-200"
234
+ style={{ fontFamily: 'inherit' }}
235
+ >
236
+ {action.label}
237
+ </button>
238
+ </TooltipTrigger>
239
+ {action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
240
+ </Tooltip>
241
+ ))}
242
+
222
243
  {micSupported && (
223
244
  <button
224
245
  onClick={toggleRecording}