@agent-relay/dashboard 2.0.82 → 2.0.84

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 (228) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
  3. package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
  4. package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
  5. package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
  6. package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
  7. package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
  8. package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
  9. package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
  10. package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
  11. package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
  12. package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
  13. package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
  14. package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
  15. package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
  16. package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
  17. package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
  18. package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
  19. package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
  20. package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
  21. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
  22. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
  23. package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
  24. package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
  25. package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
  26. package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
  27. package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
  28. package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
  29. package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
  30. package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
  31. package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
  32. package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
  33. package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
  34. package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
  35. package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
  36. package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
  37. package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
  38. package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
  39. package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
  40. package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
  41. package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
  42. package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
  43. package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
  44. package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
  45. package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
  46. package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
  47. package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
  48. package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
  49. package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
  50. package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
  51. package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
  52. package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
  53. package/out/about.html +2 -2
  54. package/out/about.txt +2 -2
  55. package/out/app/onboarding.html +1 -1
  56. package/out/app/onboarding.txt +2 -2
  57. package/out/app.html +1 -1
  58. package/out/app.txt +2 -2
  59. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
  60. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  61. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  62. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  63. package/out/blog.html +2 -2
  64. package/out/blog.txt +1 -1
  65. package/out/careers.html +2 -2
  66. package/out/careers.txt +2 -2
  67. package/out/changelog.html +2 -2
  68. package/out/changelog.txt +2 -2
  69. package/out/cloud/link.html +1 -1
  70. package/out/cloud/link.txt +2 -2
  71. package/out/complete-profile.html +2 -2
  72. package/out/complete-profile.txt +2 -2
  73. package/out/connect-repos.html +1 -1
  74. package/out/connect-repos.txt +2 -2
  75. package/out/contact.html +2 -2
  76. package/out/contact.txt +2 -2
  77. package/out/dev/cli-tools.html +1 -0
  78. package/out/dev/cli-tools.txt +7 -0
  79. package/out/dev/log-viewer.html +23 -0
  80. package/out/dev/log-viewer.txt +7 -0
  81. package/out/docs.html +2 -2
  82. package/out/docs.txt +2 -2
  83. package/out/history.html +1 -1
  84. package/out/history.txt +2 -2
  85. package/out/index.html +1 -1
  86. package/out/index.txt +2 -2
  87. package/out/login.html +2 -2
  88. package/out/login.txt +2 -2
  89. package/out/metrics.html +1 -1
  90. package/out/metrics.txt +2 -2
  91. package/out/pricing.html +2 -2
  92. package/out/pricing.txt +2 -2
  93. package/out/privacy.html +2 -2
  94. package/out/privacy.txt +2 -2
  95. package/out/providers/setup/claude.html +1 -1
  96. package/out/providers/setup/claude.txt +2 -2
  97. package/out/providers/setup/codex.html +1 -1
  98. package/out/providers/setup/codex.txt +2 -2
  99. package/out/providers/setup/cursor.html +1 -1
  100. package/out/providers/setup/cursor.txt +2 -2
  101. package/out/providers.html +1 -1
  102. package/out/providers.txt +2 -2
  103. package/out/security.html +2 -2
  104. package/out/security.txt +2 -2
  105. package/out/signup.html +2 -2
  106. package/out/signup.txt +2 -2
  107. package/out/terms.html +2 -2
  108. package/out/terms.txt +2 -2
  109. package/package.json +5 -1
  110. package/src/adapters/DashboardConfigProvider.tsx +56 -0
  111. package/src/adapters/cloudFetchAdapter.ts +278 -0
  112. package/src/adapters/index.ts +3 -0
  113. package/src/adapters/types.ts +508 -0
  114. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
  115. package/src/app/app/onboarding/page.tsx +870 -170
  116. package/src/app/cloud/link/page.tsx +14 -6
  117. package/src/app/connect-repos/page.tsx +9 -3
  118. package/src/app/dev/cli-tools/page.tsx +130 -0
  119. package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
  120. package/src/app/dev/log-viewer/fixtures.ts +110 -0
  121. package/src/app/dev/log-viewer/page.tsx +288 -0
  122. package/src/app/history/page.tsx +28 -12
  123. package/src/app/page.tsx +1 -1
  124. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
  125. package/src/components/AgentCard.tsx +4 -4
  126. package/src/components/AgentLogPreview.tsx +2 -38
  127. package/src/components/App.tsx +441 -2624
  128. package/src/components/CliToolHarness.test.tsx +83 -0
  129. package/src/components/CliToolHarness.tsx +292 -0
  130. package/src/components/CoordinatorPanel.tsx +13 -6
  131. package/src/components/LogViewer.tsx +2 -42
  132. package/src/components/ProviderAuthFlow.tsx +201 -81
  133. package/src/components/ProvisioningProgress.tsx +1 -1
  134. package/src/components/ReactionChips.tsx +2 -1
  135. package/src/components/SpawnModal.test.tsx +51 -18
  136. package/src/components/SpawnModal.tsx +175 -207
  137. package/src/components/TerminalProviderSetup.tsx +1 -1
  138. package/src/components/ThreadPanel.tsx +2 -0
  139. package/src/components/WorkspaceContext.tsx +7 -19
  140. package/src/components/XTermLogViewer.tsx +190 -27
  141. package/src/components/channels/ChannelMessageList.tsx +94 -4
  142. package/src/components/channels/ChannelViewV1.tsx +35 -11
  143. package/src/components/channels/api.ts +21 -20
  144. package/src/components/channels/types.ts +16 -0
  145. package/src/components/hooks/index.ts +0 -19
  146. package/src/components/hooks/useMessages.test.ts +80 -0
  147. package/src/components/hooks/useMessages.ts +13 -4
  148. package/src/components/hooks/useOrchestrator.ts +1 -1
  149. package/src/components/hooks/usePresence.ts +45 -6
  150. package/src/components/hooks/useThread.ts +83 -46
  151. package/src/components/hooks/useTrajectory.ts +62 -5
  152. package/src/components/hooks/useWebSocket.test.ts +358 -0
  153. package/src/components/hooks/useWebSocket.ts +243 -5
  154. package/src/components/index.ts +2 -14
  155. package/src/components/layout/Header.tsx +9 -15
  156. package/src/components/layout/Sidebar.tsx +1 -8
  157. package/src/components/settings/SettingsPage.tsx +108 -47
  158. package/src/components/settings/index.ts +0 -3
  159. package/src/landing/blogData.ts +1 -1
  160. package/src/lib/agent-merge.test.ts +2 -2
  161. package/src/lib/api.ts +8 -38
  162. package/src/lib/identity.test.ts +139 -0
  163. package/src/lib/identity.ts +48 -0
  164. package/src/lib/relaycastMessageAdapters.test.ts +182 -0
  165. package/src/lib/relaycastMessageAdapters.ts +105 -0
  166. package/src/lib/sanitize-logs.test.ts +227 -0
  167. package/src/lib/sanitize-logs.ts +202 -0
  168. package/src/providers/AgentProvider.tsx +799 -0
  169. package/src/providers/ChannelProvider.tsx +528 -0
  170. package/src/providers/CloudWorkspaceProvider.tsx +402 -0
  171. package/src/providers/MessageProvider.tsx +875 -0
  172. package/src/providers/RelayConfigProvider.tsx +94 -0
  173. package/src/providers/SendProvider.tsx +497 -0
  174. package/src/providers/SettingsProvider.tsx +247 -0
  175. package/src/providers/index.ts +26 -0
  176. package/src/types/index.ts +10 -10
  177. package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
  178. package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
  179. package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
  180. package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
  181. package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
  182. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
  183. package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
  184. package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
  185. package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
  186. package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
  187. package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
  188. package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
  189. package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
  190. package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
  191. package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
  192. package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
  193. package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
  194. package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
  195. package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
  196. package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
  197. package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
  198. package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
  199. package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
  200. package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
  201. package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
  202. package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
  203. package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
  204. package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
  205. package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
  206. package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
  207. package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
  208. package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
  209. package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
  210. package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
  211. package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
  212. package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
  213. package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
  214. package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
  215. package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
  216. package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
  217. package/src/components/BillingResult.tsx +0 -447
  218. package/src/components/CloudSessionProvider.tsx +0 -130
  219. package/src/components/SessionExpiredModal.tsx +0 -128
  220. package/src/components/WorkspaceStatusIndicator.tsx +0 -396
  221. package/src/components/hooks/useSession.ts +0 -209
  222. package/src/components/hooks/useWorkspaceMembers.ts +0 -132
  223. package/src/components/hooks/useWorkspaceStatus.ts +0 -237
  224. package/src/components/settings/BillingSettingsPanel.tsx +0 -564
  225. package/src/components/settings/TeamSettingsPanel.tsx +0 -560
  226. package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
  227. package/src/lib/cloudApi.ts +0 -893
  228. /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_ssgManifest.js +0 -0
@@ -0,0 +1,94 @@
1
+ 'use client';
2
+
3
+ import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
4
+ import { RelayProvider } from '@relaycast/react';
5
+
6
+ interface RelayConfigResponse {
7
+ success: boolean;
8
+ baseUrl?: string;
9
+ apiKey?: string;
10
+ agentToken?: string;
11
+ agentName?: string | null;
12
+ }
13
+
14
+ export interface RelayConfigProviderProps {
15
+ children: React.ReactNode;
16
+ }
17
+
18
+ interface RelayConfigStatus {
19
+ configured: boolean;
20
+ loading: boolean;
21
+ agentName: string | null;
22
+ }
23
+
24
+ const RelayConfigStatusContext = createContext<RelayConfigStatus>({
25
+ configured: false,
26
+ loading: true,
27
+ agentName: null,
28
+ });
29
+
30
+ export function useRelayConfigStatus(): RelayConfigStatus {
31
+ return useContext(RelayConfigStatusContext);
32
+ }
33
+
34
+ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
35
+ const [config, setConfig] = useState<RelayConfigResponse | null>(null);
36
+ const [loaded, setLoaded] = useState(false);
37
+
38
+ useEffect(() => {
39
+ let cancelled = false;
40
+
41
+ void fetch('/api/relay-config', { credentials: 'include' })
42
+ .then(async (response) => {
43
+ if (!response.ok) return null;
44
+ return response.json() as Promise<RelayConfigResponse>;
45
+ })
46
+ .then((payload) => {
47
+ if (cancelled || !payload?.success) return;
48
+ if (!payload.baseUrl || !payload.apiKey || !payload.agentToken) return;
49
+ setConfig(payload);
50
+ })
51
+ .catch(() => {
52
+ // No relay-config is a valid local fallback.
53
+ })
54
+ .finally(() => {
55
+ if (!cancelled) {
56
+ setLoaded(true);
57
+ }
58
+ });
59
+
60
+ return () => {
61
+ cancelled = true;
62
+ };
63
+ }, []);
64
+
65
+ const configured = Boolean(config?.baseUrl && config.apiKey && config.agentToken);
66
+ const providerConfig = useMemo(() => {
67
+ if (configured) {
68
+ return {
69
+ baseUrl: config!.baseUrl!,
70
+ apiKey: config!.apiKey!,
71
+ agentToken: config!.agentToken!,
72
+ };
73
+ }
74
+
75
+ return {
76
+ baseUrl: typeof window !== 'undefined' ? window.location.origin : 'http://127.0.0.1',
77
+ apiKey: '__relay_disabled__',
78
+ agentToken: '__relay_disabled__',
79
+ };
80
+ }, [configured, config]);
81
+
82
+ return (
83
+ <RelayConfigStatusContext.Provider value={{ configured, loading: !loaded, agentName: config?.agentName ?? null }}>
84
+ <RelayProvider
85
+ baseUrl={providerConfig.baseUrl}
86
+ apiKey={providerConfig.apiKey}
87
+ agentToken={providerConfig.agentToken}
88
+ >
89
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
90
+ {children as any}
91
+ </RelayProvider>
92
+ </RelayConfigStatusContext.Provider>
93
+ );
94
+ }
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Send Provider
3
+ *
4
+ * Manages send operations for channel messages, DM messages, reactions,
5
+ * and optimistic message creation. Extracted from the monolithic MessageProvider
6
+ * to isolate send concerns.
7
+ */
8
+
9
+ import React, { createContext, useContext, useCallback, useMemo, useRef, useEffect } from 'react';
10
+ import {
11
+ useMessages as useRelayMessages,
12
+ useSendMessage as useRelaySendMessage,
13
+ useReaction as useRelayReaction,
14
+ useAgent as useRelayAgent,
15
+ sortMessagesChronologically,
16
+ } from '@relaycast/react';
17
+ import { useCloudWorkspace } from './CloudWorkspaceProvider';
18
+ import { useRelayConfigStatus } from './RelayConfigProvider';
19
+ import { useChannelContext } from './ChannelProvider';
20
+ import { api } from '../lib/api';
21
+ import {
22
+ sendMessage as sendChannelApiMessage,
23
+ markRead,
24
+ type Channel,
25
+ type ChannelMessage as ChannelApiMessage,
26
+ type UnreadState,
27
+ } from '../components/channels';
28
+ import {
29
+ mapRelayMessageToChannelApiMessage,
30
+ } from '../lib/relaycastMessageAdapters';
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ function toChannelMessageTimestampMs(message: ChannelApiMessage): number {
37
+ const parsed = Date.parse(message.timestamp);
38
+ return Number.isFinite(parsed) ? parsed : 0;
39
+ }
40
+
41
+ function sortChannelMessagesChronologically(messages: ChannelApiMessage[]): ChannelApiMessage[] {
42
+ return [...messages].sort((a, b) => {
43
+ const tsDiff = toChannelMessageTimestampMs(a) - toChannelMessageTimestampMs(b);
44
+ if (tsDiff !== 0) return tsDiff;
45
+ return a.id.localeCompare(b.id);
46
+ });
47
+ }
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Types
51
+ // ---------------------------------------------------------------------------
52
+
53
+ export interface SendContextValue {
54
+ // Channel message sending
55
+ handleSendChannelMessage: (content: string, threadId?: string, attachmentIds?: string[]) => Promise<boolean>;
56
+ handleLoadMoreMessages: () => Promise<void>;
57
+ handleMarkChannelRead: (channelId: string) => void;
58
+
59
+ // Reactions
60
+ handleReaction: (messageId: string, emoji: string, hasReacted: boolean) => Promise<void>;
61
+
62
+ // Channel message state (managed here because of optimistic updates)
63
+ channelMessages: ChannelApiMessage[];
64
+ hasMoreMessages: boolean;
65
+ channelUnreadState: UnreadState | undefined;
66
+ effectiveChannelMessages: ChannelApiMessage[];
67
+
68
+ // Append channel message (for external WebSocket event updates)
69
+ appendChannelMessage: (channelId: string, message: ChannelApiMessage, options?: { incrementUnread?: boolean }) => void;
70
+
71
+ // Relay mapped channel messages (needed for thread reply eligibility checks)
72
+ relayMappedChannelMessages: ChannelApiMessage[];
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Context
77
+ // ---------------------------------------------------------------------------
78
+
79
+ const SendContext = createContext<SendContextValue | null>(null);
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Provider
83
+ // ---------------------------------------------------------------------------
84
+
85
+ export interface SendProviderProps {
86
+ children: React.ReactNode;
87
+ /** Messages from the local relay (non-channel) for fallback rendering */
88
+ localMessages: import('../types').Message[];
89
+ /** The local username (for sender name in optimistic messages) */
90
+ localUsername: string | null;
91
+ }
92
+
93
+ export function SendProvider({ children, localMessages, localUsername }: SendProviderProps) {
94
+ const { currentUser, effectiveActiveWorkspaceId, isWorkspaceFeaturesEnabled } = useCloudWorkspace();
95
+ const { configured: relayConfigured, agentName: relayAgentName } = useRelayConfigStatus();
96
+ const {
97
+ channelsList,
98
+ setChannelsList,
99
+ selectedChannelId,
100
+ } = useChannelContext();
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Channel message state (owned here for optimistic updates)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ const [channelMessages, setChannelMessages] = React.useState<ChannelApiMessage[]>([]);
107
+ const [channelMessageMap, setChannelMessageMap] = React.useState<Record<string, ChannelApiMessage[]>>({});
108
+ const fetchedChannelsRef = useRef<Set<string>>(new Set());
109
+ const [hasMoreMessages, setHasMoreMessages] = React.useState(false);
110
+ const [channelUnreadState, setChannelUnreadState] = React.useState<UnreadState | undefined>();
111
+ const relayRealtimeEnabledRef = useRef(false);
112
+
113
+ // Relay hooks
114
+ const relaySelectedChannelName = selectedChannelId?.startsWith('#') ? selectedChannelId.slice(1) : 'general';
115
+ const relayMessagesState = useRelayMessages(relaySelectedChannelName);
116
+ const relaySendMessageState = useRelaySendMessage();
117
+ const relayReactionState = useRelayReaction();
118
+ const relayAgent = useRelayAgent();
119
+
120
+ const relayMappedChannelMessages = useMemo(() => {
121
+ if (!relayConfigured || !selectedChannelId?.startsWith('#')) return [];
122
+ return sortMessagesChronologically(relayMessagesState.messages).map((message) =>
123
+ mapRelayMessageToChannelApiMessage(
124
+ selectedChannelId,
125
+ message,
126
+ currentUser?.displayName,
127
+ ),
128
+ );
129
+ }, [selectedChannelId, relayMessagesState.messages, currentUser?.displayName, relayConfigured]);
130
+
131
+ const usingRelayChannelMessages = Boolean(relayConfigured && selectedChannelId?.startsWith('#'));
132
+
133
+ // Local channel messages (relay messages -> channel format)
134
+ const localChannelMessages = useMemo((): ChannelApiMessage[] => {
135
+ if (effectiveActiveWorkspaceId || !selectedChannelId) return [];
136
+ const ACTIVITY_FEED_ID = '__activity__';
137
+
138
+ const filtered = localMessages.filter(m => {
139
+ if (selectedChannelId === ACTIVITY_FEED_ID) return false;
140
+ if (m.to === selectedChannelId) return true;
141
+ if (m.channel === selectedChannelId) return true;
142
+ if (m.thread === selectedChannelId) return true;
143
+ return false;
144
+ });
145
+
146
+ return filtered.map(m => ({
147
+ id: m.id,
148
+ channelId: selectedChannelId,
149
+ from: m.from,
150
+ fromEntityType: (m.from === 'Dashboard' || m.from === relayAgentName || m.from === currentUser?.displayName) ? 'user' : 'agent' as const,
151
+ content: m.content,
152
+ timestamp: m.timestamp,
153
+ isRead: m.isRead ?? true,
154
+ threadId: m.thread !== selectedChannelId ? m.thread : undefined,
155
+ }));
156
+ }, [localMessages, selectedChannelId, effectiveActiveWorkspaceId, currentUser?.displayName, relayAgentName]);
157
+
158
+ const effectiveChannelMessages = useMemo(() => {
159
+ const sourceMessages = usingRelayChannelMessages
160
+ ? relayMappedChannelMessages
161
+ : (channelMessages.length > 0 ? channelMessages : localChannelMessages);
162
+ return sortChannelMessagesChronologically(sourceMessages);
163
+ }, [usingRelayChannelMessages, relayMappedChannelMessages, channelMessages, localChannelMessages]);
164
+
165
+ useEffect(() => {
166
+ relayRealtimeEnabledRef.current = relayConfigured;
167
+ }, [relayConfigured]);
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Duplicate detection
171
+ // ---------------------------------------------------------------------------
172
+
173
+ const isDuplicateMessage = useCallback((existing: ChannelApiMessage[], message: ChannelApiMessage) => {
174
+ return existing.some((m) => {
175
+ if (m.id === message.id) return true;
176
+ if (m.from !== message.from) return false;
177
+ if (m.content !== message.content) return false;
178
+ if (m.threadId !== message.threadId) return false;
179
+ const timeDiff = Math.abs(new Date(m.timestamp).getTime() - new Date(message.timestamp).getTime());
180
+ return timeDiff < 2000;
181
+ });
182
+ }, []);
183
+
184
+ const appendChannelMessage = useCallback((channelId: string, message: ChannelApiMessage, options?: { incrementUnread?: boolean }) => {
185
+ const incrementUnread = options?.incrementUnread ?? true;
186
+
187
+ setChannelMessageMap(prev => {
188
+ const list = prev[channelId] ?? [];
189
+ if (isDuplicateMessage(list, message)) return prev;
190
+ const updated = [...list, message].sort(
191
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
192
+ );
193
+ return { ...prev, [channelId]: updated };
194
+ });
195
+
196
+ if (selectedChannelId === channelId) {
197
+ setChannelMessages(prev => {
198
+ if (isDuplicateMessage(prev, message)) return prev;
199
+ const updated = [...prev, message].sort(
200
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
201
+ );
202
+ return updated;
203
+ });
204
+ setChannelUnreadState(undefined);
205
+ } else if (incrementUnread) {
206
+ setChannelsList(prev => {
207
+ const existing = prev.find(c => c.id === channelId);
208
+ if (existing) {
209
+ return prev.map(c =>
210
+ c.id === channelId
211
+ ? { ...c, unreadCount: (c.unreadCount ?? 0) + 1 }
212
+ : c
213
+ );
214
+ }
215
+
216
+ const newChannel: Channel = {
217
+ id: channelId,
218
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
219
+ visibility: 'public',
220
+ status: 'active',
221
+ createdAt: new Date().toISOString(),
222
+ createdBy: currentUser?.displayName || relayAgentName || 'Dashboard',
223
+ memberCount: 1,
224
+ unreadCount: 1,
225
+ hasMentions: false,
226
+ isDm: channelId.startsWith('dm:'),
227
+ };
228
+
229
+ return [...prev, newChannel];
230
+ });
231
+ }
232
+ }, [currentUser?.displayName, selectedChannelId, isDuplicateMessage, setChannelsList, relayAgentName]);
233
+
234
+ // ---------------------------------------------------------------------------
235
+ // Reset channel message state when switching workspaces
236
+ // ---------------------------------------------------------------------------
237
+
238
+ useEffect(() => {
239
+ setChannelMessageMap({});
240
+ setChannelMessages([]);
241
+ fetchedChannelsRef.current.clear();
242
+ }, [effectiveActiveWorkspaceId]);
243
+
244
+ // ---------------------------------------------------------------------------
245
+ // Load messages when a channel is selected
246
+ // ---------------------------------------------------------------------------
247
+
248
+ useEffect(() => {
249
+ if (!selectedChannelId) return;
250
+ const ACTIVITY_FEED_ID = '__activity__';
251
+ if (selectedChannelId === ACTIVITY_FEED_ID) return;
252
+ if (isWorkspaceFeaturesEnabled && !effectiveActiveWorkspaceId) return;
253
+
254
+ if (relayConfigured && selectedChannelId.startsWith('#')) {
255
+ setChannelMessages(relayMappedChannelMessages);
256
+ setHasMoreMessages(false);
257
+ setChannelUnreadState(undefined);
258
+ setChannelsList(prev =>
259
+ prev.map(c =>
260
+ c.id === selectedChannelId ? { ...c, unreadCount: 0, hasMentions: false } : c
261
+ )
262
+ );
263
+ return;
264
+ }
265
+
266
+ const existing = sortChannelMessagesChronologically(channelMessageMap[selectedChannelId] ?? []);
267
+ if (existing.length > 0) {
268
+ setChannelMessages(existing);
269
+ setHasMoreMessages(false);
270
+ } else if (!fetchedChannelsRef.current.has(selectedChannelId)) {
271
+ const channelToFetch = selectedChannelId;
272
+ fetchedChannelsRef.current.add(channelToFetch);
273
+ (async () => {
274
+ try {
275
+ const { getMessages } = await import('../components/channels');
276
+ const response = await getMessages(effectiveActiveWorkspaceId || 'local', channelToFetch, { limit: 200 });
277
+ const sortedMessages = sortChannelMessagesChronologically(response.messages);
278
+ setChannelMessageMap(prev => ({ ...prev, [channelToFetch]: sortedMessages }));
279
+ setChannelMessages(sortedMessages);
280
+ setHasMoreMessages(response.hasMore);
281
+ } catch (err) {
282
+ console.error('Failed to fetch channel messages:', err);
283
+ fetchedChannelsRef.current.delete(channelToFetch);
284
+ setChannelMessages([]);
285
+ setHasMoreMessages(false);
286
+ }
287
+ })();
288
+ } else {
289
+ setChannelMessages([]);
290
+ setHasMoreMessages(false);
291
+ }
292
+
293
+ setChannelUnreadState(undefined);
294
+ setChannelsList(prev =>
295
+ prev.map(c =>
296
+ c.id === selectedChannelId ? { ...c, unreadCount: 0, hasMentions: false } : c
297
+ )
298
+ );
299
+ }, [
300
+ selectedChannelId,
301
+ effectiveActiveWorkspaceId,
302
+ relayConfigured,
303
+ relayMessagesState,
304
+ relayMappedChannelMessages,
305
+ channelMessageMap,
306
+ isWorkspaceFeaturesEnabled,
307
+ setChannelsList,
308
+ ]);
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // Send channel message
312
+ // ---------------------------------------------------------------------------
313
+
314
+ const handleSendChannelMessage = useCallback(async (content: string, threadId?: string, attachmentIds?: string[]) => {
315
+ if (!selectedChannelId) return false;
316
+
317
+ const senderName = currentUser?.displayName || localUsername || relayAgentName
318
+ || (typeof window !== 'undefined' ? localStorage.getItem('relay_username') : null)
319
+ || 'You';
320
+ const optimisticMessage: ChannelApiMessage = {
321
+ id: `local-${Date.now()}`,
322
+ channelId: selectedChannelId,
323
+ from: senderName,
324
+ fromEntityType: 'user',
325
+ content,
326
+ timestamp: new Date().toISOString(),
327
+ threadId,
328
+ isRead: true,
329
+ };
330
+
331
+ appendChannelMessage(selectedChannelId, optimisticMessage, { incrementUnread: false });
332
+
333
+ try {
334
+ const relayEligible = relayConfigured && selectedChannelId.startsWith('#');
335
+ const hasAttachments = Boolean(attachmentIds && attachmentIds.length > 0);
336
+ const relayThreadReplyEligible = threadId
337
+ ? relayMappedChannelMessages.some((message) => message.id === threadId)
338
+ : false;
339
+
340
+ if (relayEligible && !hasAttachments) {
341
+ if (threadId && relayThreadReplyEligible) {
342
+ await relayAgent.reply(threadId, content);
343
+ } else if (!threadId) {
344
+ await relaySendMessageState.send(selectedChannelId.slice(1), content);
345
+ } else {
346
+ await sendChannelApiMessage(
347
+ effectiveActiveWorkspaceId || 'local',
348
+ selectedChannelId,
349
+ { content, threadId, attachmentIds }
350
+ );
351
+ }
352
+ } else {
353
+ await sendChannelApiMessage(
354
+ effectiveActiveWorkspaceId || 'local',
355
+ selectedChannelId,
356
+ { content, threadId, attachmentIds }
357
+ );
358
+ }
359
+ return true;
360
+ } catch (err) {
361
+ console.error('Failed to send channel message:', err);
362
+ return false;
363
+ }
364
+ }, [
365
+ effectiveActiveWorkspaceId,
366
+ selectedChannelId,
367
+ currentUser?.displayName,
368
+ appendChannelMessage,
369
+ relayConfigured,
370
+ relayMappedChannelMessages,
371
+ relayAgent,
372
+ relaySendMessageState.send,
373
+ localUsername,
374
+ relayAgentName,
375
+ ]);
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // Load more messages
379
+ // ---------------------------------------------------------------------------
380
+
381
+ const handleLoadMoreMessages = useCallback(async () => {
382
+ if (relayConfigured && selectedChannelId?.startsWith('#')) {
383
+ await relayMessagesState.fetchMore();
384
+ return;
385
+ }
386
+ return;
387
+ }, [relayConfigured, relayMessagesState.fetchMore, selectedChannelId]);
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Mark channel as read (debounced)
391
+ // ---------------------------------------------------------------------------
392
+
393
+ const markReadTimeoutRef = useRef<NodeJS.Timeout | null>(null);
394
+ const handleMarkChannelRead = useCallback((channelId: string) => {
395
+ if (!effectiveActiveWorkspaceId) return;
396
+
397
+ if (markReadTimeoutRef.current) {
398
+ clearTimeout(markReadTimeoutRef.current);
399
+ }
400
+
401
+ markReadTimeoutRef.current = setTimeout(async () => {
402
+ try {
403
+ await markRead(effectiveActiveWorkspaceId, channelId);
404
+ setChannelUnreadState(undefined);
405
+ setChannelsList(prev => prev.map(c =>
406
+ c.id === channelId ? { ...c, unreadCount: 0, hasMentions: false } : c
407
+ ));
408
+ } catch (err) {
409
+ console.error('Failed to mark channel as read:', err);
410
+ }
411
+ }, 500);
412
+ }, [effectiveActiveWorkspaceId, setChannelsList]);
413
+
414
+ useEffect(() => {
415
+ if (!selectedChannelId || !channelUnreadState || channelUnreadState.count === 0) return;
416
+ handleMarkChannelRead(selectedChannelId);
417
+ }, [selectedChannelId, channelUnreadState, handleMarkChannelRead]);
418
+
419
+ useEffect(() => {
420
+ return () => {
421
+ if (markReadTimeoutRef.current) {
422
+ clearTimeout(markReadTimeoutRef.current);
423
+ }
424
+ };
425
+ }, []);
426
+
427
+ // ---------------------------------------------------------------------------
428
+ // Reactions
429
+ // ---------------------------------------------------------------------------
430
+
431
+ const handleReaction = useCallback(async (messageId: string, emoji: string, hasReacted: boolean) => {
432
+ try {
433
+ if (relayConfigured) {
434
+ if (hasReacted) {
435
+ await relayReactionState.unreact(messageId, emoji);
436
+ } else {
437
+ await relayReactionState.react(messageId, emoji);
438
+ }
439
+ return;
440
+ }
441
+
442
+ if (hasReacted) {
443
+ await api.removeReaction(messageId, emoji);
444
+ } else {
445
+ await api.addReaction(messageId, emoji);
446
+ }
447
+ } catch (err) {
448
+ console.error('Failed to update reaction:', err);
449
+ }
450
+ }, [relayConfigured, relayReactionState]);
451
+
452
+ // ---------------------------------------------------------------------------
453
+ // Context value
454
+ // ---------------------------------------------------------------------------
455
+
456
+ const value = useMemo<SendContextValue>(() => ({
457
+ handleSendChannelMessage,
458
+ handleLoadMoreMessages,
459
+ handleMarkChannelRead,
460
+ handleReaction,
461
+ channelMessages,
462
+ hasMoreMessages,
463
+ channelUnreadState,
464
+ effectiveChannelMessages,
465
+ appendChannelMessage,
466
+ relayMappedChannelMessages,
467
+ }), [
468
+ handleSendChannelMessage,
469
+ handleLoadMoreMessages,
470
+ handleMarkChannelRead,
471
+ handleReaction,
472
+ channelMessages,
473
+ hasMoreMessages,
474
+ channelUnreadState,
475
+ effectiveChannelMessages,
476
+ appendChannelMessage,
477
+ relayMappedChannelMessages,
478
+ ]);
479
+
480
+ return (
481
+ <SendContext.Provider value={value}>
482
+ {children}
483
+ </SendContext.Provider>
484
+ );
485
+ }
486
+
487
+ // ---------------------------------------------------------------------------
488
+ // Hook
489
+ // ---------------------------------------------------------------------------
490
+
491
+ export function useSendContext(): SendContextValue {
492
+ const ctx = useContext(SendContext);
493
+ if (!ctx) {
494
+ throw new Error('useSendContext must be used within a SendProvider');
495
+ }
496
+ return ctx;
497
+ }