@agent-relay/dashboard 2.0.81 → 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/{dYlczDQI12PIQ3tqq3N4Y → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{dYlczDQI12PIQ3tqq3N4Y → 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,367 @@
1
+ /**
2
+ * useChannels Hook
3
+ *
4
+ * Manages channel-based messaging via the presence WebSocket.
5
+ * - Join/leave channels
6
+ * - Send/receive channel messages
7
+ * - Send/receive direct messages
8
+ * - Track joined channels
9
+ */
10
+
11
+ import { useState, useEffect, useCallback, useRef } from 'react';
12
+ import { getWebSocketUrl } from '../../lib/config';
13
+
14
+ /** Channel message from server */
15
+ export interface ChannelMessage {
16
+ id: string;
17
+ type: 'channel_message' | 'direct_message';
18
+ channel?: string;
19
+ from: string;
20
+ to?: string;
21
+ body: string;
22
+ thread?: string;
23
+ timestamp: string;
24
+ }
25
+
26
+ export interface UseChannelsOptions {
27
+ /** Current user info (if logged in) */
28
+ currentUser?: {
29
+ username: string;
30
+ avatarUrl?: string;
31
+ };
32
+ /** WebSocket URL (defaults to same as main WebSocket) */
33
+ wsUrl?: string;
34
+ /** Whether to auto-connect */
35
+ autoConnect?: boolean;
36
+ /** Callback when a message is received */
37
+ onMessage?: (message: ChannelMessage) => void;
38
+ /** Workspace ID for cloud channel message subscription */
39
+ workspaceId?: string;
40
+ }
41
+
42
+ /** Connection quality state for UI indicators */
43
+ export type ChannelConnectionState = 'connected' | 'reconnecting' | 'disconnected';
44
+
45
+ export interface UseChannelsReturn {
46
+ /** List of channels user has joined */
47
+ channels: string[];
48
+ /** Join a channel */
49
+ joinChannel: (channel: string) => void;
50
+ /** Leave a channel */
51
+ leaveChannel: (channel: string) => void;
52
+ /** Send a message to a channel */
53
+ sendChannelMessage: (channel: string, body: string, thread?: string) => void;
54
+ /** Send a direct message */
55
+ sendDirectMessage: (to: string, body: string, thread?: string) => void;
56
+ /** Whether connected */
57
+ isConnected: boolean;
58
+ /** Granular connection quality: 'connected', 'reconnecting', or 'disconnected' */
59
+ connectionState: ChannelConnectionState;
60
+ /** Recent messages (last 100) */
61
+ messages: ChannelMessage[];
62
+ }
63
+
64
+ /**
65
+ * Get the presence WebSocket URL using centralized config
66
+ */
67
+ function getPresenceUrl(): string {
68
+ return getWebSocketUrl('/ws/presence');
69
+ }
70
+
71
+ const MAX_MESSAGES = 100;
72
+
73
+ export function useChannels(options: UseChannelsOptions = {}): UseChannelsReturn {
74
+ const { currentUser, wsUrl, autoConnect = true, onMessage, workspaceId } = options;
75
+
76
+ const [channels, setChannels] = useState<string[]>([]);
77
+ const [messages, setMessages] = useState<ChannelMessage[]>([]);
78
+ const [isConnected, setIsConnected] = useState(false);
79
+ const [connectionState, setConnectionState] = useState<ChannelConnectionState>('disconnected');
80
+
81
+ const wsRef = useRef<WebSocket | null>(null);
82
+ const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
83
+ const reconnectAttemptsRef = useRef(0);
84
+ const isConnectingRef = useRef(false);
85
+ const hasConnectedBeforeRef = useRef(false);
86
+ const currentUserRef = useRef(currentUser);
87
+ const onMessageRef = useRef(onMessage);
88
+ const workspaceIdRef = useRef(workspaceId);
89
+ currentUserRef.current = currentUser;
90
+ onMessageRef.current = onMessage;
91
+ workspaceIdRef.current = workspaceId;
92
+
93
+ const connect = useCallback(() => {
94
+ const user = currentUserRef.current;
95
+ if (!user) return;
96
+ if (wsRef.current?.readyState === WebSocket.OPEN) return;
97
+ if (isConnectingRef.current) return;
98
+
99
+ // Track reconnection state
100
+ if (hasConnectedBeforeRef.current) {
101
+ setConnectionState('reconnecting');
102
+ }
103
+
104
+ isConnectingRef.current = true;
105
+ const url = wsUrl || getPresenceUrl();
106
+
107
+ try {
108
+ const ws = new WebSocket(url);
109
+
110
+ ws.onopen = () => {
111
+ isConnectingRef.current = false;
112
+ setIsConnected(true);
113
+ setConnectionState('connected');
114
+ reconnectAttemptsRef.current = 0;
115
+ hasConnectedBeforeRef.current = true;
116
+
117
+ const currentUserInfo = currentUserRef.current;
118
+ if (currentUserInfo) {
119
+ // Announce presence (this registers with UserBridge on server)
120
+ ws.send(JSON.stringify({
121
+ type: 'presence',
122
+ action: 'join',
123
+ user: {
124
+ username: currentUserInfo.username,
125
+ avatarUrl: currentUserInfo.avatarUrl,
126
+ },
127
+ }));
128
+
129
+ // Subscribe to channel messages for this workspace (cloud mode)
130
+ const wsId = workspaceIdRef.current;
131
+ if (wsId) {
132
+ ws.send(JSON.stringify({
133
+ type: 'subscribe_channels',
134
+ workspaceId: wsId,
135
+ }));
136
+ }
137
+ }
138
+ };
139
+
140
+ ws.onclose = () => {
141
+ isConnectingRef.current = false;
142
+ setIsConnected(false);
143
+ wsRef.current = null;
144
+
145
+ if (currentUserRef.current) {
146
+ setConnectionState('reconnecting');
147
+ const baseDelay = Math.min(
148
+ 500 * Math.pow(2, reconnectAttemptsRef.current),
149
+ 15000
150
+ );
151
+ // Add jitter to prevent thundering herd
152
+ const delay = Math.round(baseDelay * (0.5 + Math.random() * 0.5));
153
+ reconnectAttemptsRef.current++;
154
+
155
+ console.log(`[WS:Channels] Reconnecting (attempt ${reconnectAttemptsRef.current})...`);
156
+
157
+ reconnectTimeoutRef.current = setTimeout(() => {
158
+ connect();
159
+ }, delay);
160
+ } else {
161
+ setConnectionState('disconnected');
162
+ }
163
+ };
164
+
165
+ ws.onerror = (event) => {
166
+ console.error('[useChannels] Error:', event);
167
+ };
168
+
169
+ ws.onmessage = (event) => {
170
+ try {
171
+ const msg = JSON.parse(event.data);
172
+
173
+ switch (msg.type) {
174
+ case 'channel_joined':
175
+ if (msg.success) {
176
+ setChannels((prev) => {
177
+ if (prev.includes(msg.channel)) return prev;
178
+ return [...prev, msg.channel];
179
+ });
180
+ }
181
+ break;
182
+
183
+ case 'channel_left':
184
+ if (msg.success) {
185
+ setChannels((prev) => prev.filter((c) => c !== msg.channel));
186
+ }
187
+ break;
188
+
189
+ case 'channel_message': {
190
+ const channelMsg: ChannelMessage = {
191
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
192
+ type: 'channel_message',
193
+ channel: msg.channel,
194
+ from: msg.from,
195
+ body: msg.body,
196
+ thread: msg.thread,
197
+ timestamp: msg.timestamp || new Date().toISOString(),
198
+ };
199
+ setMessages((prev) => {
200
+ const updated = [...prev, channelMsg];
201
+ return updated.slice(-MAX_MESSAGES);
202
+ });
203
+ onMessageRef.current?.(channelMsg);
204
+ break;
205
+ }
206
+
207
+ case 'direct_message': {
208
+ const dmMsg: ChannelMessage = {
209
+ id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
210
+ type: 'direct_message',
211
+ from: msg.from,
212
+ to: currentUserRef.current?.username,
213
+ body: msg.body,
214
+ thread: msg.thread,
215
+ timestamp: msg.timestamp || new Date().toISOString(),
216
+ };
217
+ setMessages((prev) => {
218
+ const updated = [...prev, dmMsg];
219
+ return updated.slice(-MAX_MESSAGES);
220
+ });
221
+ onMessageRef.current?.(dmMsg);
222
+ break;
223
+ }
224
+ }
225
+ } catch (e) {
226
+ console.error('[useChannels] Failed to parse message:', e);
227
+ }
228
+ };
229
+
230
+ wsRef.current = ws;
231
+ } catch (e) {
232
+ console.error('[useChannels] Failed to create WebSocket:', e);
233
+ }
234
+ }, [wsUrl]);
235
+
236
+ const disconnect = useCallback(() => {
237
+ if (reconnectTimeoutRef.current) {
238
+ clearTimeout(reconnectTimeoutRef.current);
239
+ reconnectTimeoutRef.current = null;
240
+ }
241
+
242
+ isConnectingRef.current = false;
243
+
244
+ if (wsRef.current) {
245
+ const ws = wsRef.current;
246
+ ws.onclose = null;
247
+ ws.onerror = null;
248
+
249
+ const user = currentUserRef.current;
250
+ if (ws.readyState === WebSocket.OPEN && user) {
251
+ ws.send(JSON.stringify({
252
+ type: 'presence',
253
+ action: 'leave',
254
+ username: user.username,
255
+ }));
256
+ }
257
+ ws.close();
258
+ wsRef.current = null;
259
+ }
260
+
261
+ setIsConnected(false);
262
+ setConnectionState('disconnected');
263
+ setChannels([]);
264
+ }, []);
265
+
266
+ const joinChannel = useCallback((channel: string) => {
267
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
268
+
269
+ wsRef.current.send(JSON.stringify({
270
+ type: 'channel_join',
271
+ channel,
272
+ }));
273
+ }, []);
274
+
275
+ const leaveChannel = useCallback((channel: string) => {
276
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
277
+
278
+ wsRef.current.send(JSON.stringify({
279
+ type: 'channel_leave',
280
+ channel,
281
+ }));
282
+ }, []);
283
+
284
+ const sendChannelMessage = useCallback((channel: string, body: string, thread?: string) => {
285
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
286
+
287
+ wsRef.current.send(JSON.stringify({
288
+ type: 'channel_message',
289
+ channel,
290
+ body,
291
+ thread,
292
+ }));
293
+ }, []);
294
+
295
+ const sendDirectMessage = useCallback((to: string, body: string, thread?: string) => {
296
+ if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return;
297
+
298
+ wsRef.current.send(JSON.stringify({
299
+ type: 'direct_message',
300
+ to,
301
+ body,
302
+ thread,
303
+ }));
304
+ }, []);
305
+
306
+ // Connect when user is available
307
+ useEffect(() => {
308
+ if (!autoConnect || !currentUserRef.current) return;
309
+
310
+ if (wsRef.current && wsRef.current.readyState !== WebSocket.CLOSED) {
311
+ return;
312
+ }
313
+
314
+ connect();
315
+
316
+ return () => {
317
+ disconnect();
318
+ };
319
+ }, [autoConnect, currentUser?.username, workspaceId, connect, disconnect]);
320
+
321
+ // Send leave on page unload
322
+ useEffect(() => {
323
+ const handleUnload = () => {
324
+ const user = currentUserRef.current;
325
+ if (wsRef.current?.readyState === WebSocket.OPEN && user) {
326
+ wsRef.current.send(JSON.stringify({
327
+ type: 'presence',
328
+ action: 'leave',
329
+ username: user.username,
330
+ }));
331
+ }
332
+ };
333
+
334
+ window.addEventListener('beforeunload', handleUnload);
335
+ return () => window.removeEventListener('beforeunload', handleUnload);
336
+ }, []);
337
+
338
+ // Visibility change listener: reconnect when tab becomes visible
339
+ useEffect(() => {
340
+ const handleVisibilityChange = () => {
341
+ if (document.visibilityState === 'visible') {
342
+ // Check if connection is dead and reconnect
343
+ if (currentUserRef.current && (!wsRef.current || wsRef.current.readyState === WebSocket.CLOSED)) {
344
+ console.log('[WS:Channels] Tab visible, reconnecting...');
345
+ reconnectAttemptsRef.current = 0; // Reset attempts for visibility-triggered reconnect
346
+ connect();
347
+ }
348
+ }
349
+ };
350
+
351
+ document.addEventListener('visibilitychange', handleVisibilityChange);
352
+ return () => {
353
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
354
+ };
355
+ }, [connect]);
356
+
357
+ return {
358
+ channels,
359
+ joinChannel,
360
+ leaveChannel,
361
+ sendChannelMessage,
362
+ sendDirectMessage,
363
+ isConnected,
364
+ connectionState,
365
+ messages,
366
+ };
367
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * useDebounce Hook
3
+ *
4
+ * Returns a debounced value that only updates after a delay.
5
+ * Useful for search inputs and other rapid-change scenarios.
6
+ */
7
+
8
+ import { useState, useEffect } from 'react';
9
+
10
+ /**
11
+ * Debounce a value by a specified delay.
12
+ * @param value - The value to debounce
13
+ * @param delay - Delay in milliseconds (default: 300ms)
14
+ */
15
+ export function useDebounce<T>(value: T, delay: number = 300): T {
16
+ const [debouncedValue, setDebouncedValue] = useState(value);
17
+
18
+ useEffect(() => {
19
+ const handler = setTimeout(() => {
20
+ setDebouncedValue(value);
21
+ }, delay);
22
+
23
+ return () => {
24
+ clearTimeout(handler);
25
+ };
26
+ }, [value, delay]);
27
+
28
+ return debouncedValue;
29
+ }