@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
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import type { BoardTask } from '@/types'
5
+ import { api } from '@/lib/api-client'
6
+ import { useAppStore } from '@/stores/use-app-store'
7
+ import { toast } from 'sonner'
8
+
9
+ interface Props {
10
+ task: BoardTask
11
+ }
12
+
13
+ export function TaskApprovalCard({ task }: Props) {
14
+ const [resolving, setResolving] = useState(false)
15
+ const loadTasks = useAppStore((s) => s.loadTasks)
16
+ const loadSessions = useAppStore((s) => s.loadSessions)
17
+
18
+ const handleResolve = async (approved: boolean) => {
19
+ setResolving(true)
20
+ try {
21
+ await api('POST', `/tasks/${task.id}/approve`, { approved })
22
+ toast.success(approved ? 'Tool execution approved' : 'Tool execution rejected')
23
+ await loadTasks()
24
+ await loadSessions()
25
+ } catch (err: unknown) {
26
+ toast.error(err instanceof Error ? err.message : 'Failed to submit decision')
27
+ } finally {
28
+ setResolving(false)
29
+ }
30
+ }
31
+
32
+ if (!task.pendingApproval) return null
33
+
34
+ const { toolName, args } = task.pendingApproval
35
+
36
+ return (
37
+ <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">
38
+ <div className="flex items-center gap-2 mb-3">
39
+ <div className="w-5 h-5 rounded-full bg-amber-500/20 flex items-center justify-center">
40
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-amber-400">
41
+ <path d="M12 9v2m0 4h.01" />
42
+ <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" />
43
+ </svg>
44
+ </div>
45
+ <span className="text-[12px] font-700 text-amber-400 uppercase tracking-wider">Orchestration Intervention Required</span>
46
+ </div>
47
+
48
+ <p className="text-[13px] text-text-2 mb-3 font-500">
49
+ Agent requested to use <span className="text-amber-400 font-600 font-mono">{toolName}</span>:
50
+ </p>
51
+
52
+ <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">
53
+ <pre className="text-[11px] font-mono text-text-2/80 leading-relaxed whitespace-pre-wrap break-all">
54
+ {JSON.stringify(args, null, 2)}
55
+ </pre>
56
+ </div>
57
+
58
+ <div className="flex items-center gap-2">
59
+ <button
60
+ onClick={() => handleResolve(true)}
61
+ disabled={resolving}
62
+ 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"
63
+ style={{ fontFamily: 'inherit' }}
64
+ >
65
+ {resolving ? 'Applying...' : 'Approve'}
66
+ </button>
67
+ <button
68
+ onClick={() => handleResolve(false)}
69
+ disabled={resolving}
70
+ 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"
71
+ style={{ fontFamily: 'inherit' }}
72
+ >
73
+ Reject
74
+ </button>
75
+ </div>
76
+ </div>
77
+ )
78
+ }
@@ -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} />
@@ -29,6 +29,7 @@ const TOOL_COLORS: Record<string, string> = {
29
29
  search_history_tool: '#8B5CF6',
30
30
  manage_tasks: '#EC4899',
31
31
  manage_schedules: '#EC4899',
32
+ schedule_wake: '#F59E0B',
32
33
  manage_agents: '#EC4899',
33
34
  manage_skills: '#EC4899',
34
35
  manage_documents: '#EC4899',
@@ -81,6 +82,7 @@ export const TOOL_LABELS: Record<string, string> = {
81
82
  search_history_tool: 'Search History',
82
83
  manage_tasks: 'Tasks',
83
84
  manage_schedules: 'Schedules',
85
+ schedule_wake: 'Set Reminder',
84
86
  manage_agents: 'Agents',
85
87
  manage_skills: 'Skills',
86
88
  manage_documents: 'Documents',
@@ -118,6 +120,7 @@ export const TOOL_DESCRIPTIONS: Record<string, string> = {
118
120
  search_history_tool: 'Search chat history for relevant prior context',
119
121
  manage_tasks: 'Create, update, and manage tasks on the board',
120
122
  manage_schedules: 'Create and manage cron schedules',
123
+ schedule_wake: 'Set a timer to wake up in this chat later',
121
124
  manage_agents: 'Create and configure other agents',
122
125
  manage_skills: 'Create and manage agent skills',
123
126
  manage_documents: 'Upload and search indexed documents',
@@ -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'
@@ -12,6 +12,7 @@ import { AgentPickerList } from '@/components/shared/agent-picker-list'
12
12
  import { ChatroomPickerList } from '@/components/shared/chatroom-picker-list'
13
13
  import { SheetFooter } from '@/components/shared/sheet-footer'
14
14
  import { SectionLabel } from '@/components/shared/section-label'
15
+ import { HintTip } from '@/components/shared/hint-tip'
15
16
  import { useChatroomStore } from '@/stores/use-chatroom-store'
16
17
  import { ConnectorHealth } from '@/components/connectors/connector-health'
17
18
 
@@ -241,6 +242,29 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
241
242
  export function ConnectorSheet() {
242
243
  const open = useAppStore((s) => s.connectorSheetOpen)
243
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])
244
268
  const editingId = useAppStore((s) => s.editingConnectorId)
245
269
  const setEditingId = useAppStore((s) => s.setEditingConnectorId)
246
270
  const connectors = useAppStore((s) => s.connectors)
@@ -392,7 +416,7 @@ export function ConnectorSheet() {
392
416
  setEditingId(null)
393
417
  }
394
418
 
395
- const platformConfig = PLATFORMS.find((p) => p.id === platform)!
419
+ const platformConfig = ALL_PLATFORMS.find((p) => p.id === platform) || ALL_PLATFORMS[0]
396
420
  const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
397
421
  const credList = Object.values(credentials)
398
422
 
@@ -412,17 +436,17 @@ export function ConnectorSheet() {
412
436
  <div className="mb-8">
413
437
  <SectionLabel>Platform</SectionLabel>
414
438
  <div className="grid grid-cols-2 gap-3">
415
- {PLATFORMS.map((p) => (
439
+ {ALL_PLATFORMS.map((p) => (
416
440
  <button
417
441
  key={p.id}
418
- onClick={() => { setPlatform(p.id); setShowSetup(false) }}
442
+ onClick={() => { setPlatform(p.id as ConnectorPlatform); setShowSetup(false) }}
419
443
  className={`flex items-center gap-3 p-4 rounded-[14px] cursor-pointer transition-all duration-200 border text-left
420
444
  ${platform === p.id
421
445
  ? 'bg-white/[0.04] border-white/[0.15] shadow-[0_0_20px_rgba(255,255,255,0.02)]'
422
446
  : 'bg-transparent border-white/[0.04] hover:border-white/[0.08] hover:bg-white/[0.01]'}`}
423
447
  style={{ fontFamily: 'inherit' }}
424
448
  >
425
- <ConnectorPlatformBadge platform={p.id} size={40} iconSize={18} />
449
+ <ConnectorPlatformBadge platform={p.id as ConnectorPlatform} size={40} iconSize={18} />
426
450
  <div>
427
451
  <div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
428
452
  <div className="text-[11px] text-text-3 mt-0.5">
@@ -438,7 +462,7 @@ export function ConnectorSheet() {
438
462
  {/* Editing: show platform badge */}
439
463
  {editing && (
440
464
  <div className="mb-6 flex items-center gap-3">
441
- <ConnectorPlatformBadge platform={platformConfig.id} size={40} iconSize={18} />
465
+ <ConnectorPlatformBadge platform={platformConfig.id as ConnectorPlatform} size={40} iconSize={18} />
442
466
  <div>
443
467
  <div className="text-[14px] font-600 text-text">{platformConfig.label}</div>
444
468
  <div className="flex items-center gap-2 mt-0.5">
@@ -643,12 +667,18 @@ export function ConnectorSheet() {
643
667
  {/* Platform-specific config */}
644
668
  {[...platformConfig.configFields, ...COMMON_CONFIG_FIELDS].map((field) => {
645
669
  const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
670
+ const fieldHint: Record<string, string> = {
671
+ channelIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
672
+ chatIds: "Find these in your platform's developer settings. Leave empty to allow all channels",
673
+ allowedJids: "Phone numbers in international format (e.g. 447xxx). Leave empty to allow all",
674
+ }
646
675
  if (isTagField) {
647
676
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
648
677
  return (
649
678
  <div key={field.key} className="mb-6">
650
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
679
+ <label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
651
680
  {field.label} <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
681
+ {fieldHint[field.key] && <HintTip text={fieldHint[field.key]} />}
652
682
  </label>
653
683
  {field.help && <p className="text-[12px] text-text-3/60 mb-2">{field.help}</p>}
654
684
  <div className="flex flex-wrap gap-2 mb-2">