@agent-relay/dashboard 2.0.80 → 2.0.82

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 (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
@@ -0,0 +1,473 @@
1
+ /**
2
+ * MessageComposer - Shared message input component with attachment support
3
+ *
4
+ * Features:
5
+ * - Image paste from clipboard
6
+ * - File upload via button
7
+ * - @-mention autocomplete (optional)
8
+ * - File path autocomplete (optional)
9
+ * - Typing indicator support
10
+ * - Multi-line support (Shift+Enter)
11
+ *
12
+ * Used by both DMs and Channels for consistent messaging experience.
13
+ */
14
+
15
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
16
+ import { api } from '../lib/api';
17
+ import { MentionAutocomplete, getMentionQuery, type HumanUser } from './MentionAutocomplete';
18
+ import { FileAutocomplete, getFileQuery } from './FileAutocomplete';
19
+ import type { Agent } from '../types';
20
+
21
+ /**
22
+ * Pending attachment state during upload
23
+ */
24
+ export interface PendingAttachment {
25
+ id: string;
26
+ file: File;
27
+ preview: string;
28
+ isUploading: boolean;
29
+ uploadedId?: string;
30
+ error?: string;
31
+ }
32
+
33
+ /**
34
+ * Props for the MessageComposer component
35
+ */
36
+ export interface MessageComposerProps {
37
+ /** Called when user sends a message */
38
+ onSend: (content: string, attachmentIds?: string[]) => Promise<boolean>;
39
+ /** Called when typing state changes */
40
+ onTyping?: (isTyping: boolean) => void;
41
+ /** Whether a send is in progress */
42
+ isSending?: boolean;
43
+ /** Whether input is disabled */
44
+ disabled?: boolean;
45
+ /** Placeholder text */
46
+ placeholder?: string;
47
+ /** Error message to display */
48
+ error?: string | null;
49
+ /** Agent list for @-mention autocomplete */
50
+ agents?: Agent[];
51
+ /** Human user list for @-mention autocomplete */
52
+ humanUsers?: HumanUser[];
53
+ /** Enable file path autocomplete */
54
+ enableFileAutocomplete?: boolean;
55
+ /** Mention to insert (triggered externally) */
56
+ insertMention?: string;
57
+ /** Called after mention is inserted */
58
+ onMentionInserted?: () => void;
59
+ /** Custom class for the form container */
60
+ className?: string;
61
+ }
62
+
63
+ export function MessageComposer({
64
+ onSend,
65
+ onTyping,
66
+ isSending = false,
67
+ disabled = false,
68
+ placeholder = 'Type a message...',
69
+ error,
70
+ agents = [],
71
+ humanUsers = [],
72
+ enableFileAutocomplete = false,
73
+ insertMention,
74
+ onMentionInserted,
75
+ className = '',
76
+ }: MessageComposerProps) {
77
+ const [message, setMessage] = useState('');
78
+ const [cursorPosition, setCursorPosition] = useState(0);
79
+ const [showMentions, setShowMentions] = useState(false);
80
+ const [showFiles, setShowFiles] = useState(false);
81
+ const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
82
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
83
+ const fileInputRef = useRef<HTMLInputElement>(null);
84
+
85
+ // Handle insertMention prop - insert @username when triggered from outside
86
+ useEffect(() => {
87
+ if (insertMention && onMentionInserted) {
88
+ const mentionText = `@${insertMention} `;
89
+ const textarea = textareaRef.current;
90
+ if (textarea) {
91
+ const start = textarea.selectionStart || message.length;
92
+ const newMessage = message.slice(0, start) + mentionText + message.slice(start);
93
+ setMessage(newMessage);
94
+ setTimeout(() => {
95
+ textarea.focus();
96
+ const newPos = start + mentionText.length;
97
+ textarea.setSelectionRange(newPos, newPos);
98
+ }, 0);
99
+ } else {
100
+ setMessage(prev => prev + mentionText);
101
+ }
102
+ onMentionInserted();
103
+ }
104
+ }, [insertMention, onMentionInserted, message]);
105
+
106
+ // Process image files (used by both paste and file input)
107
+ const processImageFiles = useCallback(async (imageFiles: File[]) => {
108
+ for (const file of imageFiles) {
109
+ const id = crypto.randomUUID();
110
+ const preview = URL.createObjectURL(file);
111
+
112
+ // Add to pending attachments
113
+ setAttachments(prev => [...prev, {
114
+ id,
115
+ file,
116
+ preview,
117
+ isUploading: true,
118
+ }]);
119
+
120
+ // Upload the file
121
+ try {
122
+ const result = await api.uploadAttachment(file);
123
+ if (result.success && result.data) {
124
+ setAttachments(prev => prev.map(a =>
125
+ a.id === id
126
+ ? { ...a, isUploading: false, uploadedId: result.data!.attachment.id }
127
+ : a
128
+ ));
129
+ } else {
130
+ setAttachments(prev => prev.map(a =>
131
+ a.id === id
132
+ ? { ...a, isUploading: false, error: result.error || 'Upload failed' }
133
+ : a
134
+ ));
135
+ }
136
+ } catch (err) {
137
+ setAttachments(prev => prev.map(a =>
138
+ a.id === id
139
+ ? { ...a, isUploading: false, error: 'Upload failed' }
140
+ : a
141
+ ));
142
+ }
143
+ }
144
+ }, []);
145
+
146
+ // Handle file selection from file input
147
+ const handleFileSelect = useCallback((files: FileList | null) => {
148
+ if (!files || files.length === 0) return;
149
+
150
+ const imageFiles = Array.from(files).filter(file =>
151
+ file.type.startsWith('image/')
152
+ );
153
+
154
+ if (imageFiles.length > 0) {
155
+ processImageFiles(imageFiles);
156
+ }
157
+ }, [processImageFiles]);
158
+
159
+ // Handle paste for clipboard images
160
+ const handlePaste = useCallback((e: React.ClipboardEvent) => {
161
+ const clipboardData = e.clipboardData;
162
+ if (!clipboardData) return;
163
+
164
+ let imageFiles: File[] = [];
165
+
166
+ // Method 1: Check clipboardData.files (works for file pastes)
167
+ if (clipboardData.files && clipboardData.files.length > 0) {
168
+ imageFiles = Array.from(clipboardData.files).filter(file =>
169
+ file.type.startsWith('image/')
170
+ );
171
+ }
172
+
173
+ // Method 2: Check clipboardData.items (works for screenshots/copied images)
174
+ if (imageFiles.length === 0 && clipboardData.items) {
175
+ const items = Array.from(clipboardData.items);
176
+ for (const item of items) {
177
+ if (item.kind === 'file' && item.type.startsWith('image/')) {
178
+ const file = item.getAsFile();
179
+ if (file) {
180
+ imageFiles.push(file);
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ // Process any found images
187
+ if (imageFiles.length > 0) {
188
+ e.preventDefault();
189
+ processImageFiles(imageFiles);
190
+ }
191
+ }, [processImageFiles]);
192
+
193
+ // Remove an attachment
194
+ const removeAttachment = useCallback((id: string) => {
195
+ setAttachments(prev => {
196
+ const attachment = prev.find(a => a.id === id);
197
+ if (attachment) {
198
+ URL.revokeObjectURL(attachment.preview);
199
+ }
200
+ return prev.filter(a => a.id !== id);
201
+ });
202
+ }, []);
203
+
204
+ const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
205
+ const value = e.target.value;
206
+ const cursorPos = e.target.selectionStart || 0;
207
+ setMessage(value);
208
+ setCursorPosition(cursorPos);
209
+
210
+ // Send typing indicator when user has content
211
+ onTyping?.(value.trim().length > 0);
212
+
213
+ // Check for file autocomplete first (@ followed by path-like pattern)
214
+ if (enableFileAutocomplete) {
215
+ const fileQuery = getFileQuery(value, cursorPos);
216
+ if (fileQuery !== null) {
217
+ setShowFiles(true);
218
+ setShowMentions(false);
219
+ return;
220
+ }
221
+ }
222
+
223
+ // Check for mention autocomplete (@ at start without path patterns)
224
+ if (agents.length > 0 || humanUsers.length > 0) {
225
+ const mentionQuery = getMentionQuery(value, cursorPos);
226
+ if (mentionQuery !== null) {
227
+ setShowMentions(true);
228
+ setShowFiles(false);
229
+ return;
230
+ }
231
+ }
232
+
233
+ // Neither - hide both
234
+ setShowMentions(false);
235
+ setShowFiles(false);
236
+ };
237
+
238
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
239
+ // Don't handle Enter/Tab when autocomplete is visible
240
+ if ((showMentions || showFiles) && (e.key === 'ArrowDown' || e.key === 'ArrowUp' || e.key === 'Tab')) {
241
+ return;
242
+ }
243
+
244
+ // Option/Alt+Enter: insert newline manually (browser doesn't do this by default)
245
+ if (e.key === 'Enter' && e.altKey && !showMentions && !showFiles) {
246
+ e.preventDefault();
247
+ const textarea = e.currentTarget;
248
+ const start = textarea.selectionStart;
249
+ const end = textarea.selectionEnd;
250
+ const newValue = message.slice(0, start) + '\n' + message.slice(end);
251
+ setMessage(newValue);
252
+ // Set cursor position after the newline
253
+ setTimeout(() => {
254
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
255
+ }, 0);
256
+ return;
257
+ }
258
+
259
+ // Enter without modifiers: send message
260
+ if (e.key === 'Enter' && !e.shiftKey && !e.altKey && !showMentions && !showFiles) {
261
+ e.preventDefault();
262
+ if ((message.trim() || attachments.length > 0) && !isSending && !disabled) {
263
+ handleSubmit(e as unknown as React.FormEvent);
264
+ }
265
+ }
266
+ };
267
+
268
+ const handleMentionSelect = (mention: string, newValue: string) => {
269
+ setMessage(newValue);
270
+ setShowMentions(false);
271
+ setShowFiles(false);
272
+ setTimeout(() => {
273
+ if (textareaRef.current) {
274
+ textareaRef.current.focus();
275
+ const pos = newValue.indexOf(' ') + 1;
276
+ textareaRef.current.setSelectionRange(pos, pos);
277
+ }
278
+ }, 0);
279
+ };
280
+
281
+ const handleFilePathSelect = (filePath: string, newValue: string) => {
282
+ setMessage(newValue);
283
+ setShowFiles(false);
284
+ setShowMentions(false);
285
+ setTimeout(() => {
286
+ if (textareaRef.current) {
287
+ textareaRef.current.focus();
288
+ const pos = newValue.indexOf(' ', 1) + 1;
289
+ textareaRef.current.setSelectionRange(pos, pos);
290
+ }
291
+ }, 0);
292
+ };
293
+
294
+ const handleSubmit = async (e: React.FormEvent) => {
295
+ e.preventDefault();
296
+
297
+ const hasMessage = message.trim().length > 0;
298
+ const hasAttachments = attachments.length > 0;
299
+ if ((!hasMessage && !hasAttachments) || isSending || disabled) return;
300
+
301
+ // Check if any attachments are still uploading
302
+ const stillUploading = attachments.some(a => a.isUploading);
303
+ if (stillUploading) return;
304
+
305
+ // Get uploaded attachment IDs
306
+ const attachmentIds = attachments
307
+ .filter(a => a.uploadedId)
308
+ .map(a => a.uploadedId!);
309
+
310
+ // If no message but has attachments, send with default text
311
+ let content = message.trim();
312
+ if (!content && attachmentIds.length > 0) {
313
+ content = '[Screenshot attached]';
314
+ }
315
+
316
+ const success = await onSend(
317
+ content,
318
+ attachmentIds.length > 0 ? attachmentIds : undefined
319
+ );
320
+
321
+ if (success) {
322
+ // Clean up previews
323
+ attachments.forEach(a => URL.revokeObjectURL(a.preview));
324
+ setMessage('');
325
+ setAttachments([]);
326
+ setShowMentions(false);
327
+ setShowFiles(false);
328
+ }
329
+ };
330
+
331
+ // Check if we can send
332
+ const canSend = (message.trim() || attachments.length > 0) &&
333
+ !isSending &&
334
+ !disabled &&
335
+ !attachments.some(a => a.isUploading);
336
+
337
+ return (
338
+ <form className={`flex flex-col gap-1.5 sm:gap-2 ${className}`} onSubmit={handleSubmit}>
339
+ {/* Attachment previews */}
340
+ {attachments.length > 0 && (
341
+ <div className="flex flex-wrap gap-1.5 sm:gap-2 p-1.5 sm:p-2 bg-bg-card rounded-lg border border-border-subtle">
342
+ {attachments.map(attachment => (
343
+ <div key={attachment.id} className="relative group">
344
+ <img
345
+ src={attachment.preview}
346
+ alt={attachment.file.name}
347
+ className={`h-16 w-auto rounded-lg object-cover ${attachment.isUploading ? 'opacity-50' : ''} ${attachment.error ? 'border-2 border-error' : ''}`}
348
+ />
349
+ {attachment.isUploading && (
350
+ <div className="absolute inset-0 flex items-center justify-center">
351
+ <svg className="animate-spin h-5 w-5 text-accent-cyan" viewBox="0 0 24 24">
352
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="2" fill="none" strokeDasharray="32" strokeLinecap="round" />
353
+ </svg>
354
+ </div>
355
+ )}
356
+ {attachment.error && (
357
+ <div className="absolute bottom-0 left-0 right-0 bg-error/90 text-white text-[10px] px-1 py-0.5 truncate">
358
+ {attachment.error}
359
+ </div>
360
+ )}
361
+ <button
362
+ type="button"
363
+ onClick={() => removeAttachment(attachment.id)}
364
+ className="absolute -top-1.5 -right-1.5 w-5 h-5 bg-bg-tertiary border border-border-subtle rounded-full flex items-center justify-center text-text-muted hover:text-error hover:border-error transition-colors opacity-0 group-hover:opacity-100"
365
+ title="Remove"
366
+ >
367
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
368
+ <line x1="18" y1="6" x2="6" y2="18" />
369
+ <line x1="6" y1="6" x2="18" y2="18" />
370
+ </svg>
371
+ </button>
372
+ </div>
373
+ ))}
374
+ </div>
375
+ )}
376
+
377
+ {/* Input row */}
378
+ <div className="flex items-center gap-1.5 sm:gap-3">
379
+ {/* Image upload button */}
380
+ <input
381
+ ref={fileInputRef}
382
+ type="file"
383
+ accept="image/*"
384
+ multiple
385
+ className="hidden"
386
+ onChange={(e) => handleFileSelect(e.target.files)}
387
+ />
388
+ <button
389
+ type="button"
390
+ onClick={() => fileInputRef.current?.click()}
391
+ disabled={disabled}
392
+ className="p-2 sm:p-2.5 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-text-muted hover:text-accent-cyan hover:border-accent-cyan/50 transition-colors flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
393
+ title="Attach screenshot (or paste from clipboard)"
394
+ >
395
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="sm:w-[18px] sm:h-[18px]">
396
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
397
+ <circle cx="8.5" cy="8.5" r="1.5" />
398
+ <polyline points="21 15 16 10 5 21" />
399
+ </svg>
400
+ </button>
401
+
402
+ <div className="flex-1 relative min-w-0">
403
+ {/* Agent mention autocomplete */}
404
+ {(agents.length > 0 || humanUsers.length > 0) && (
405
+ <MentionAutocomplete
406
+ agents={agents}
407
+ humanUsers={humanUsers}
408
+ inputValue={message}
409
+ cursorPosition={cursorPosition}
410
+ onSelect={handleMentionSelect}
411
+ onClose={() => setShowMentions(false)}
412
+ isVisible={showMentions}
413
+ />
414
+ )}
415
+ {/* File path autocomplete */}
416
+ {enableFileAutocomplete && (
417
+ <FileAutocomplete
418
+ inputValue={message}
419
+ cursorPosition={cursorPosition}
420
+ onSelect={handleFilePathSelect}
421
+ onClose={() => setShowFiles(false)}
422
+ isVisible={showFiles}
423
+ />
424
+ )}
425
+ <textarea
426
+ ref={textareaRef}
427
+ className="w-full py-2 sm:py-3 px-3 sm:px-4 bg-bg-card border border-border-subtle rounded-lg sm:rounded-xl text-sm font-sans text-text-primary outline-none transition-all duration-200 resize-none min-h-[40px] sm:min-h-[44px] max-h-[100px] sm:max-h-[120px] overflow-y-auto focus:border-accent-cyan/50 focus:shadow-[0_0_0_3px_rgba(0,217,255,0.1)] placeholder:text-text-muted disabled:opacity-50 disabled:cursor-not-allowed"
428
+ placeholder={placeholder}
429
+ value={message}
430
+ onChange={handleInputChange}
431
+ onKeyDown={handleKeyDown}
432
+ onPaste={handlePaste}
433
+ onSelect={(e) => setCursorPosition((e.target as HTMLTextAreaElement).selectionStart || 0)}
434
+ disabled={disabled || isSending}
435
+ rows={1}
436
+ />
437
+ </div>
438
+ <button
439
+ type="submit"
440
+ className="py-2 sm:py-3 px-3 sm:px-5 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold border-none rounded-lg sm:rounded-xl text-xs sm:text-sm cursor-pointer transition-all duration-150 hover:shadow-glow-cyan hover:-translate-y-0.5 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:translate-y-0 disabled:hover:shadow-none flex-shrink-0"
441
+ disabled={!canSend}
442
+ title={isSending ? 'Sending...' : attachments.some(a => a.isUploading) ? 'Uploading...' : 'Send message'}
443
+ >
444
+ {isSending ? (
445
+ <span className="hidden sm:inline">Sending...</span>
446
+ ) : attachments.some(a => a.isUploading) ? (
447
+ <span className="hidden sm:inline">Uploading...</span>
448
+ ) : (
449
+ <span className="flex items-center gap-1 sm:gap-2">
450
+ <span className="hidden sm:inline">Send</span>
451
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
452
+ <line x1="22" y1="2" x2="11" y2="13"></line>
453
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
454
+ </svg>
455
+ </span>
456
+ )}
457
+ </button>
458
+ {error && <span className="text-error text-xs ml-2">{error}</span>}
459
+ </div>
460
+
461
+ {/* Helper text */}
462
+ <p className="text-xs text-text-muted px-1">
463
+ <kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">Enter</kbd> to send,{' '}
464
+ <kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">Shift/Option+Enter</kbd> for new line
465
+ {(agents.length > 0 || humanUsers.length > 0) && (
466
+ <>, <kbd className="px-1 py-0.5 bg-bg-tertiary rounded text-[10px]">@</kbd> to mention</>
467
+ )}
468
+ </p>
469
+ </form>
470
+ );
471
+ }
472
+
473
+ export default MessageComposer;