@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,528 @@
1
+ /**
2
+ * Channel Provider
3
+ *
4
+ * Manages channel CRUD operations, channel list state, channel selection,
5
+ * and channel modal state. Extracted from the monolithic MessageProvider
6
+ * to keep channel management concerns focused in one place.
7
+ */
8
+
9
+ import React, { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
10
+ import {
11
+ useChannels as useRelayChannels,
12
+ } from '@relaycast/react';
13
+ import { useCloudWorkspace } from './CloudWorkspaceProvider';
14
+ import { useRelayConfigStatus } from './RelayConfigProvider';
15
+ import { getCsrfToken } from '../lib/api';
16
+ import {
17
+ listChannels,
18
+ getChannelMembers,
19
+ removeMember as removeChannelMember,
20
+ createChannel,
21
+ type Channel,
22
+ type ChannelMember,
23
+ type CreateChannelRequest,
24
+ } from '../components/channels';
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Types
28
+ // ---------------------------------------------------------------------------
29
+
30
+ type RelaycastChannel = ReturnType<typeof useRelayChannels>['channels'][number];
31
+
32
+ function mapRelayChannelToDashboard(channel: RelaycastChannel): Channel {
33
+ const channelName = channel.name;
34
+ return {
35
+ id: `#${channelName}`,
36
+ name: channelName,
37
+ description: channel.topic ?? undefined,
38
+ topic: channel.topic ?? undefined,
39
+ visibility: 'public',
40
+ status: channel.isArchived ? 'archived' : 'active',
41
+ createdAt: channel.createdAt ?? new Date().toISOString(),
42
+ createdBy: 'relay',
43
+ memberCount: channel.memberCount ?? 0,
44
+ unreadCount: 0,
45
+ hasMentions: false,
46
+ isDm: false,
47
+ };
48
+ }
49
+
50
+ export interface ChannelContextValue {
51
+ // Channel list state
52
+ channelsList: Channel[];
53
+ archivedChannelsList: Channel[];
54
+ isChannelsLoading: boolean;
55
+ selectedChannelId: string | undefined;
56
+ setSelectedChannelId: React.Dispatch<React.SetStateAction<string | undefined>>;
57
+ selectedChannel: Channel | undefined;
58
+
59
+ // Channel handlers
60
+ handleSelectChannel: (channel: Channel) => Promise<void>;
61
+ handleCreateChannel: () => void;
62
+ handleCreateChannelSubmit: (request: CreateChannelRequest) => Promise<void>;
63
+ handleInviteToChannel: (channel: Channel) => void;
64
+ handleInviteSubmit: (members: string[]) => Promise<void>;
65
+ handleJoinChannel: (channelId: string) => Promise<void>;
66
+ handleLeaveChannel: (channel: Channel) => Promise<void>;
67
+ handleShowMembers: () => Promise<void>;
68
+ handleRemoveMember: (memberId: string, memberType: 'user' | 'agent') => Promise<void>;
69
+ handleAddMember: (memberId: string, memberType: 'user' | 'agent', role: 'admin' | 'member' | 'read_only') => Promise<void>;
70
+ handleArchiveChannel: (channel: Channel) => Promise<void>;
71
+ handleUnarchiveChannel: (channel: Channel) => Promise<void>;
72
+
73
+ // Channel modals
74
+ isCreateChannelOpen: boolean;
75
+ setIsCreateChannelOpen: React.Dispatch<React.SetStateAction<boolean>>;
76
+ isCreatingChannel: boolean;
77
+ isInviteChannelOpen: boolean;
78
+ setIsInviteChannelOpen: React.Dispatch<React.SetStateAction<boolean>>;
79
+ inviteChannelTarget: Channel | null;
80
+ setInviteChannelTarget: React.Dispatch<React.SetStateAction<Channel | null>>;
81
+ isInvitingToChannel: boolean;
82
+ showMemberPanel: boolean;
83
+ setShowMemberPanel: React.Dispatch<React.SetStateAction<boolean>>;
84
+ channelMembers: ChannelMember[];
85
+
86
+ // Setters exposed for external updates (e.g. WebSocket events)
87
+ setChannelsList: React.Dispatch<React.SetStateAction<Channel[]>>;
88
+
89
+ // Relay mapped channels (needed by MessageProvider for messages)
90
+ relayMappedChannels: Channel[];
91
+ }
92
+
93
+ // ---------------------------------------------------------------------------
94
+ // Context
95
+ // ---------------------------------------------------------------------------
96
+
97
+ const ChannelContext = createContext<ChannelContextValue | null>(null);
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Constants
101
+ // ---------------------------------------------------------------------------
102
+
103
+ /** Special ID for the Activity feed (broadcasts) */
104
+ const ACTIVITY_FEED_ID = '__activity__';
105
+ const DEFAULT_CHANNEL_IDS = ['#general', '#engineering'];
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Provider
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export interface ChannelProviderProps {
112
+ children: React.ReactNode;
113
+ }
114
+
115
+ export function ChannelProvider({ children }: ChannelProviderProps) {
116
+ const { effectiveActiveWorkspaceId, isWorkspaceFeaturesEnabled } = useCloudWorkspace();
117
+ const { configured: relayConfigured } = useRelayConfigStatus();
118
+
119
+ // Relay channel state
120
+ const relayChannelsState = useRelayChannels();
121
+ const relayMappedChannels = useMemo(
122
+ () => relayChannelsState.channels.map(mapRelayChannelToDashboard),
123
+ [relayChannelsState.channels],
124
+ );
125
+
126
+ // Channel list state
127
+ const [channelsList, setChannelsList] = useState<Channel[]>([]);
128
+ const [archivedChannelsList, setArchivedChannelsList] = useState<Channel[]>([]);
129
+ const [isChannelsLoading, setIsChannelsLoading] = useState(false);
130
+ const [selectedChannelId, setSelectedChannelId] = useState<string | undefined>(
131
+ isWorkspaceFeaturesEnabled ? ACTIVITY_FEED_ID : '#general'
132
+ );
133
+
134
+ // Channel modals
135
+ const [isCreateChannelOpen, setIsCreateChannelOpen] = useState(false);
136
+ const [isCreatingChannel, setIsCreatingChannel] = useState(false);
137
+ const [isInviteChannelOpen, setIsInviteChannelOpen] = useState(false);
138
+ const [inviteChannelTarget, setInviteChannelTarget] = useState<Channel | null>(null);
139
+ const [isInvitingToChannel, setIsInvitingToChannel] = useState(false);
140
+ const [showMemberPanel, setShowMemberPanel] = useState(false);
141
+ const [channelMembers, setChannelMembers] = useState<ChannelMember[]>([]);
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Helpers
145
+ // ---------------------------------------------------------------------------
146
+
147
+ const setChannelListsFromResponse = useCallback((response: { channels: Channel[]; archivedChannels?: Channel[] }) => {
148
+ const archived = [
149
+ ...(response.archivedChannels || []),
150
+ ...response.channels.filter(c => c.status === 'archived'),
151
+ ];
152
+ const apiActive = response.channels.filter(c => c.status !== 'archived');
153
+
154
+ const apiChannelIds = new Set(apiActive.map(c => c.id));
155
+ const defaultChannelsToAdd: Channel[] = DEFAULT_CHANNEL_IDS
156
+ .filter(id => !apiChannelIds.has(id))
157
+ .map(id => ({
158
+ id,
159
+ name: id.replace('#', ''),
160
+ description: id === '#general' ? 'General discussion for all agents' : 'Engineering discussion',
161
+ visibility: 'public' as const,
162
+ memberCount: 0,
163
+ unreadCount: 0,
164
+ hasMentions: false,
165
+ createdAt: new Date().toISOString(),
166
+ status: 'active' as const,
167
+ createdBy: 'system',
168
+ isDm: false,
169
+ }));
170
+
171
+ setChannelsList([...defaultChannelsToAdd, ...apiActive]);
172
+ setArchivedChannelsList(archived);
173
+ }, []);
174
+
175
+ const selectedChannel = useMemo(() => {
176
+ if (!selectedChannelId) return undefined;
177
+ return channelsList.find(c => c.id === selectedChannelId) ||
178
+ archivedChannelsList.find(c => c.id === selectedChannelId);
179
+ }, [selectedChannelId, channelsList, archivedChannelsList]);
180
+
181
+ // Default channels for non-cloud mode
182
+ const defaultChannels = useMemo<Channel[]>(() => [
183
+ {
184
+ id: '#general',
185
+ name: 'general',
186
+ description: 'General discussion for all agents',
187
+ visibility: 'public',
188
+ memberCount: 0,
189
+ unreadCount: 0,
190
+ hasMentions: false,
191
+ createdAt: '2024-01-01T00:00:00.000Z',
192
+ status: 'active',
193
+ createdBy: 'system',
194
+ isDm: false,
195
+ },
196
+ {
197
+ id: '#engineering',
198
+ name: 'engineering',
199
+ description: 'Engineering discussion',
200
+ visibility: 'public',
201
+ memberCount: 0,
202
+ unreadCount: 0,
203
+ hasMentions: false,
204
+ createdAt: '2024-01-01T00:00:00.000Z',
205
+ status: 'active',
206
+ createdBy: 'system',
207
+ isDm: false,
208
+ },
209
+ ], []);
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // Reset channel state when switching workspaces
213
+ // ---------------------------------------------------------------------------
214
+
215
+ useEffect(() => {
216
+ setSelectedChannelId(undefined);
217
+ }, [effectiveActiveWorkspaceId]);
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Load channels on mount
221
+ // ---------------------------------------------------------------------------
222
+
223
+ useEffect(() => {
224
+ if (relayConfigured) {
225
+ const activeChannels = relayMappedChannels.filter((channel) => channel.status !== 'archived');
226
+ const archivedChannels = relayMappedChannels.filter((channel) => channel.status === 'archived');
227
+ setChannelListsFromResponse({ channels: activeChannels, archivedChannels });
228
+ setIsChannelsLoading(relayChannelsState.loading);
229
+ return;
230
+ }
231
+
232
+ if (!isWorkspaceFeaturesEnabled || !effectiveActiveWorkspaceId) {
233
+ setChannelsList(defaultChannels);
234
+ setArchivedChannelsList([]);
235
+ return;
236
+ }
237
+
238
+ setChannelsList(defaultChannels);
239
+ setArchivedChannelsList([]);
240
+ setIsChannelsLoading(true);
241
+
242
+ const fetchChannels = async () => {
243
+ try {
244
+ const response = await listChannels(effectiveActiveWorkspaceId);
245
+ setChannelListsFromResponse(response);
246
+ } catch (err) {
247
+ console.error('Failed to fetch channels:', err);
248
+ } finally {
249
+ setIsChannelsLoading(false);
250
+ }
251
+ };
252
+
253
+ fetchChannels();
254
+ }, [
255
+ relayConfigured,
256
+ relayChannelsState,
257
+ relayMappedChannels,
258
+ effectiveActiveWorkspaceId,
259
+ isWorkspaceFeaturesEnabled,
260
+ defaultChannels,
261
+ setChannelListsFromResponse,
262
+ ]);
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Channel handlers
266
+ // ---------------------------------------------------------------------------
267
+
268
+ const handleSelectChannel = useCallback(async (channel: Channel) => {
269
+ setSelectedChannelId(channel.id);
270
+
271
+ try {
272
+ const { joinChannel: joinChannelApi } = await import('../components/channels');
273
+ await joinChannelApi(effectiveActiveWorkspaceId || 'local', channel.id);
274
+ } catch (err) {
275
+ console.error('Failed to join channel:', err);
276
+ }
277
+ }, [effectiveActiveWorkspaceId]);
278
+
279
+ const handleCreateChannel = useCallback(() => {
280
+ setIsCreateChannelOpen(true);
281
+ }, []);
282
+
283
+ const handleCreateChannelSubmit = useCallback(async (request: CreateChannelRequest) => {
284
+ if (!effectiveActiveWorkspaceId) return;
285
+ setIsCreatingChannel(true);
286
+ try {
287
+ const result = await createChannel(effectiveActiveWorkspaceId, request);
288
+ const response = await listChannels(effectiveActiveWorkspaceId);
289
+ setChannelListsFromResponse(response);
290
+ if (result.channel?.id) {
291
+ setSelectedChannelId(result.channel.id);
292
+ }
293
+ setIsCreateChannelOpen(false);
294
+ } catch (err) {
295
+ console.error('Failed to create channel:', err);
296
+ } finally {
297
+ setIsCreatingChannel(false);
298
+ }
299
+ }, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
300
+
301
+ const handleInviteToChannel = useCallback((channel: Channel) => {
302
+ setInviteChannelTarget(channel);
303
+ setIsInviteChannelOpen(true);
304
+ }, []);
305
+
306
+ const handleInviteSubmit = useCallback(async (members: string[]) => {
307
+ if (!inviteChannelTarget) return;
308
+ setIsInvitingToChannel(true);
309
+ try {
310
+ const csrfToken = getCsrfToken();
311
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
312
+ if (csrfToken) {
313
+ headers['X-CSRF-Token'] = csrfToken;
314
+ }
315
+
316
+ const invites = members.map(name => ({ id: name, type: 'agent' as const }));
317
+
318
+ const response = await fetch('/api/channels/invite', {
319
+ method: 'POST',
320
+ headers,
321
+ credentials: 'include',
322
+ body: JSON.stringify({
323
+ channel: inviteChannelTarget.name,
324
+ invites,
325
+ workspaceId: effectiveActiveWorkspaceId,
326
+ }),
327
+ });
328
+ if (!response.ok) {
329
+ throw new Error('Failed to invite members');
330
+ }
331
+ setIsInviteChannelOpen(false);
332
+ setInviteChannelTarget(null);
333
+ } catch (err) {
334
+ console.error('Failed to invite to channel:', err);
335
+ } finally {
336
+ setIsInvitingToChannel(false);
337
+ }
338
+ }, [inviteChannelTarget, effectiveActiveWorkspaceId]);
339
+
340
+ const handleJoinChannel = useCallback(async (channelId: string) => {
341
+ if (!effectiveActiveWorkspaceId) return;
342
+ try {
343
+ const { joinChannel } = await import('../components/channels');
344
+ await joinChannel(effectiveActiveWorkspaceId, channelId);
345
+ const response = await listChannels(effectiveActiveWorkspaceId);
346
+ setChannelListsFromResponse(response);
347
+ } catch (err) {
348
+ console.error('Failed to join channel:', err);
349
+ }
350
+ }, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
351
+
352
+ const handleLeaveChannel = useCallback(async (channel: Channel) => {
353
+ if (!effectiveActiveWorkspaceId) return;
354
+ try {
355
+ const { leaveChannel } = await import('../components/channels');
356
+ await leaveChannel(effectiveActiveWorkspaceId, channel.id);
357
+ if (selectedChannelId === channel.id) {
358
+ setSelectedChannelId(undefined);
359
+ }
360
+ const response = await listChannels(effectiveActiveWorkspaceId);
361
+ setChannelListsFromResponse(response);
362
+ } catch (err) {
363
+ console.error('Failed to leave channel:', err);
364
+ }
365
+ }, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
366
+
367
+ const handleShowMembers = useCallback(async () => {
368
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
369
+ try {
370
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
371
+ setChannelMembers(members);
372
+ setShowMemberPanel(true);
373
+ } catch (err) {
374
+ console.error('Failed to load channel members:', err);
375
+ }
376
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
377
+
378
+ const handleRemoveMember = useCallback(async (memberId: string, memberType: 'user' | 'agent') => {
379
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
380
+ try {
381
+ await removeChannelMember(effectiveActiveWorkspaceId, selectedChannel.id, memberId, memberType);
382
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
383
+ setChannelMembers(members);
384
+ } catch (err) {
385
+ console.error('Failed to remove member:', err);
386
+ }
387
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
388
+
389
+ const handleAddMember = useCallback(async (memberId: string, memberType: 'user' | 'agent', _role: 'admin' | 'member' | 'read_only') => {
390
+ if (!selectedChannel || !effectiveActiveWorkspaceId) return;
391
+ try {
392
+ const csrfToken = getCsrfToken();
393
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
394
+ if (csrfToken) {
395
+ headers['X-CSRF-Token'] = csrfToken;
396
+ }
397
+
398
+ const response = await fetch('/api/channels/invite', {
399
+ method: 'POST',
400
+ headers,
401
+ credentials: 'include',
402
+ body: JSON.stringify({
403
+ channel: selectedChannel.name,
404
+ invites: [{ id: memberId, type: memberType }],
405
+ workspaceId: effectiveActiveWorkspaceId,
406
+ }),
407
+ });
408
+
409
+ if (!response.ok) {
410
+ throw new Error('Failed to add member');
411
+ }
412
+
413
+ const members = await getChannelMembers(effectiveActiveWorkspaceId, selectedChannel.id);
414
+ setChannelMembers(members);
415
+ } catch (err) {
416
+ console.error('Failed to add member:', err);
417
+ }
418
+ }, [selectedChannel, effectiveActiveWorkspaceId]);
419
+
420
+ const handleArchiveChannel = useCallback(async (channel: Channel) => {
421
+ if (!effectiveActiveWorkspaceId) return;
422
+ try {
423
+ const { archiveChannel } = await import('../components/channels');
424
+ await archiveChannel(effectiveActiveWorkspaceId, channel.id);
425
+ if (selectedChannelId === channel.id) {
426
+ setSelectedChannelId(undefined);
427
+ }
428
+ const response = await listChannels(effectiveActiveWorkspaceId);
429
+ setChannelListsFromResponse(response);
430
+ } catch (err) {
431
+ console.error('Failed to archive channel:', err);
432
+ }
433
+ }, [effectiveActiveWorkspaceId, selectedChannelId, setChannelListsFromResponse]);
434
+
435
+ const handleUnarchiveChannel = useCallback(async (channel: Channel) => {
436
+ if (!effectiveActiveWorkspaceId) return;
437
+ try {
438
+ const { unarchiveChannel } = await import('../components/channels');
439
+ await unarchiveChannel(effectiveActiveWorkspaceId, channel.id);
440
+ const response = await listChannels(effectiveActiveWorkspaceId);
441
+ setChannelListsFromResponse(response);
442
+ } catch (err) {
443
+ console.error('Failed to unarchive channel:', err);
444
+ }
445
+ }, [effectiveActiveWorkspaceId, setChannelListsFromResponse]);
446
+
447
+ // ---------------------------------------------------------------------------
448
+ // Context value
449
+ // ---------------------------------------------------------------------------
450
+
451
+ const value = useMemo<ChannelContextValue>(() => ({
452
+ channelsList,
453
+ archivedChannelsList,
454
+ isChannelsLoading,
455
+ selectedChannelId,
456
+ setSelectedChannelId,
457
+ selectedChannel,
458
+ handleSelectChannel,
459
+ handleCreateChannel,
460
+ handleCreateChannelSubmit,
461
+ handleInviteToChannel,
462
+ handleInviteSubmit,
463
+ handleJoinChannel,
464
+ handleLeaveChannel,
465
+ handleShowMembers,
466
+ handleRemoveMember,
467
+ handleAddMember,
468
+ handleArchiveChannel,
469
+ handleUnarchiveChannel,
470
+ isCreateChannelOpen,
471
+ setIsCreateChannelOpen,
472
+ isCreatingChannel,
473
+ isInviteChannelOpen,
474
+ setIsInviteChannelOpen,
475
+ inviteChannelTarget,
476
+ setInviteChannelTarget,
477
+ isInvitingToChannel,
478
+ showMemberPanel,
479
+ setShowMemberPanel,
480
+ channelMembers,
481
+ setChannelsList,
482
+ relayMappedChannels,
483
+ }), [
484
+ channelsList,
485
+ archivedChannelsList,
486
+ isChannelsLoading,
487
+ selectedChannelId,
488
+ selectedChannel,
489
+ handleSelectChannel,
490
+ handleCreateChannel,
491
+ handleCreateChannelSubmit,
492
+ handleInviteToChannel,
493
+ handleInviteSubmit,
494
+ handleJoinChannel,
495
+ handleLeaveChannel,
496
+ handleShowMembers,
497
+ handleRemoveMember,
498
+ handleAddMember,
499
+ handleArchiveChannel,
500
+ handleUnarchiveChannel,
501
+ isCreateChannelOpen,
502
+ isCreatingChannel,
503
+ isInviteChannelOpen,
504
+ inviteChannelTarget,
505
+ isInvitingToChannel,
506
+ showMemberPanel,
507
+ channelMembers,
508
+ relayMappedChannels,
509
+ ]);
510
+
511
+ return (
512
+ <ChannelContext.Provider value={value}>
513
+ {children}
514
+ </ChannelContext.Provider>
515
+ );
516
+ }
517
+
518
+ // ---------------------------------------------------------------------------
519
+ // Hook
520
+ // ---------------------------------------------------------------------------
521
+
522
+ export function useChannelContext(): ChannelContextValue {
523
+ const ctx = useContext(ChannelContext);
524
+ if (!ctx) {
525
+ throw new Error('useChannelContext must be used within a ChannelProvider');
526
+ }
527
+ return ctx;
528
+ }