@agent-relay/dashboard 2.0.80 → 2.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{118-4c8241b0218335de.js → 118-ae2b650136a5a5fc.js} +1 -1
  3. package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +1 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-1e81c047cff17212.js → page-f7eca1b66fb4249b.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-6892fe2dd07fb48b.js → page-0ee604f7070d14c0.js} +1 -1
  6. package/out/_next/static/css/8968d98ed4c4d33f.css +1 -0
  7. package/out/about.html +2 -2
  8. package/out/about.txt +1 -1
  9. package/out/app/onboarding.html +1 -1
  10. package/out/app/onboarding.txt +1 -1
  11. package/out/app.html +1 -1
  12. package/out/app.txt +2 -2
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +2 -2
  14. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
  16. package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
  17. package/out/blog.html +2 -2
  18. package/out/blog.txt +1 -1
  19. package/out/careers.html +2 -2
  20. package/out/careers.txt +1 -1
  21. package/out/changelog.html +2 -2
  22. package/out/changelog.txt +1 -1
  23. package/out/cloud/link.html +1 -1
  24. package/out/cloud/link.txt +2 -2
  25. package/out/complete-profile.html +2 -2
  26. package/out/complete-profile.txt +1 -1
  27. package/out/connect-repos.html +1 -1
  28. package/out/connect-repos.txt +1 -1
  29. package/out/contact.html +2 -2
  30. package/out/contact.txt +1 -1
  31. package/out/docs.html +2 -2
  32. package/out/docs.txt +1 -1
  33. package/out/history.html +1 -1
  34. package/out/history.txt +2 -2
  35. package/out/index.html +1 -1
  36. package/out/index.txt +2 -2
  37. package/out/login.html +2 -2
  38. package/out/login.txt +1 -1
  39. package/out/metrics.html +1 -1
  40. package/out/metrics.txt +2 -2
  41. package/out/pricing.html +2 -2
  42. package/out/pricing.txt +1 -1
  43. package/out/privacy.html +2 -2
  44. package/out/privacy.txt +1 -1
  45. package/out/providers/setup/claude.html +1 -1
  46. package/out/providers/setup/claude.txt +1 -1
  47. package/out/providers/setup/codex.html +1 -1
  48. package/out/providers/setup/codex.txt +1 -1
  49. package/out/providers/setup/cursor.html +1 -1
  50. package/out/providers/setup/cursor.txt +1 -1
  51. package/out/providers.html +1 -1
  52. package/out/providers.txt +1 -1
  53. package/out/security.html +2 -2
  54. package/out/security.txt +1 -1
  55. package/out/signup.html +2 -2
  56. package/out/signup.txt +1 -1
  57. package/out/terms.html +2 -2
  58. package/out/terms.txt +1 -1
  59. package/package.json +7 -1
  60. package/src/app/about/page.tsx +7 -0
  61. package/src/app/app/[[...slug]]/DashboardPageClient.tsx +853 -0
  62. package/src/app/app/[[...slug]]/page.tsx +23 -0
  63. package/src/app/app/onboarding/page.tsx +394 -0
  64. package/src/app/apple-icon.png +0 -0
  65. package/src/app/blog/go-to-bed-wake-up-to-a-finished-product/page.tsx +88 -0
  66. package/src/app/blog/let-them-cook-multi-agent-orchestration/page.tsx +93 -0
  67. package/src/app/blog/page.tsx +15 -0
  68. package/src/app/careers/page.tsx +7 -0
  69. package/src/app/changelog/page.tsx +7 -0
  70. package/src/app/cloud/link/page.tsx +464 -0
  71. package/src/app/complete-profile/page.tsx +204 -0
  72. package/src/app/connect-repos/page.tsx +410 -0
  73. package/src/app/contact/page.tsx +7 -0
  74. package/src/app/docs/page.tsx +7 -0
  75. package/src/app/favicon.png +0 -0
  76. package/src/app/globals.css +200 -0
  77. package/src/app/history/page.tsx +658 -0
  78. package/src/app/layout.tsx +25 -0
  79. package/src/app/login/page.tsx +424 -0
  80. package/src/app/metrics/page.tsx +781 -0
  81. package/src/app/page.tsx +59 -0
  82. package/src/app/pricing/page.tsx +7 -0
  83. package/src/app/privacy/page.tsx +7 -0
  84. package/src/app/providers/page.tsx +193 -0
  85. package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +197 -0
  86. package/src/app/providers/setup/[provider]/constants.ts +35 -0
  87. package/src/app/providers/setup/[provider]/page.tsx +42 -0
  88. package/src/app/security/page.tsx +7 -0
  89. package/src/app/signup/page.tsx +533 -0
  90. package/src/app/terms/page.tsx +7 -0
  91. package/src/components/ActivityFeed.tsx +216 -0
  92. package/src/components/AddWorkspaceModal.tsx +170 -0
  93. package/src/components/AgentCard.test.tsx +134 -0
  94. package/src/components/AgentCard.tsx +585 -0
  95. package/src/components/AgentList.test.tsx +147 -0
  96. package/src/components/AgentList.tsx +419 -0
  97. package/src/components/AgentLogPreview.tsx +173 -0
  98. package/src/components/AgentProfilePanel.tsx +569 -0
  99. package/src/components/App.tsx +3424 -0
  100. package/src/components/BillingPanel.tsx +922 -0
  101. package/src/components/BillingResult.tsx +447 -0
  102. package/src/components/BroadcastComposer.tsx +690 -0
  103. package/src/components/ChannelAdminPanel.tsx +773 -0
  104. package/src/components/ChannelBrowser.tsx +385 -0
  105. package/src/components/ChannelChat.tsx +261 -0
  106. package/src/components/ChannelSidebar.tsx +399 -0
  107. package/src/components/CloudSessionProvider.tsx +130 -0
  108. package/src/components/CommandPalette.tsx +815 -0
  109. package/src/components/ConfirmationDialog.tsx +133 -0
  110. package/src/components/ConversationHistory.tsx +518 -0
  111. package/src/components/CoordinatorPanel.tsx +956 -0
  112. package/src/components/DecisionQueue.tsx +717 -0
  113. package/src/components/DirectMessageView.tsx +164 -0
  114. package/src/components/FileAutocomplete.tsx +368 -0
  115. package/src/components/FleetOverview.tsx +278 -0
  116. package/src/components/LogViewer.tsx +310 -0
  117. package/src/components/LogViewerPanel.tsx +482 -0
  118. package/src/components/Logo.tsx +284 -0
  119. package/src/components/MentionAutocomplete.tsx +384 -0
  120. package/src/components/MessageComposer.tsx +473 -0
  121. package/src/components/MessageList.tsx +725 -0
  122. package/src/components/MessageSenderName.tsx +91 -0
  123. package/src/components/MessageStatusIndicator.tsx +142 -0
  124. package/src/components/NewConversationModal.tsx +400 -0
  125. package/src/components/NotificationToast.tsx +488 -0
  126. package/src/components/OnlineUsersIndicator.tsx +164 -0
  127. package/src/components/Pagination.tsx +124 -0
  128. package/src/components/PricingPlans.tsx +386 -0
  129. package/src/components/ProjectList.tsx +711 -0
  130. package/src/components/ProviderAuthFlow.tsx +343 -0
  131. package/src/components/ProviderConnectionList.tsx +375 -0
  132. package/src/components/ProvisioningProgress.tsx +730 -0
  133. package/src/components/ReactionChips.tsx +70 -0
  134. package/src/components/ReactionPicker.tsx +121 -0
  135. package/src/components/RepoAccessPanel.tsx +787 -0
  136. package/src/components/RepositoriesPanel.tsx +901 -0
  137. package/src/components/ServerCard.tsx +202 -0
  138. package/src/components/SessionExpiredModal.tsx +128 -0
  139. package/src/components/SpawnModal.test.tsx +190 -0
  140. package/src/components/SpawnModal.tsx +1001 -0
  141. package/src/components/TaskAssignmentUI.tsx +375 -0
  142. package/src/components/TerminalProviderSetup.tsx +517 -0
  143. package/src/components/ThemeProvider.tsx +159 -0
  144. package/src/components/ThinkingIndicator.tsx +231 -0
  145. package/src/components/ThreadList.tsx +198 -0
  146. package/src/components/ThreadPanel.tsx +405 -0
  147. package/src/components/TrajectoryViewer.tsx +698 -0
  148. package/src/components/TypingIndicator.tsx +69 -0
  149. package/src/components/UsageBanner.tsx +231 -0
  150. package/src/components/UserProfilePanel.tsx +233 -0
  151. package/src/components/WorkspaceContext.tsx +95 -0
  152. package/src/components/WorkspaceSelector.tsx +234 -0
  153. package/src/components/WorkspaceStatusIndicator.tsx +396 -0
  154. package/src/components/XTermInteractive.tsx +516 -0
  155. package/src/components/XTermLogViewer.tsx +719 -0
  156. package/src/components/channels/ChannelDialogs.tsx +1411 -0
  157. package/src/components/channels/ChannelHeader.tsx +317 -0
  158. package/src/components/channels/ChannelMessageList.tsx +463 -0
  159. package/src/components/channels/ChannelViewV1.tsx +146 -0
  160. package/src/components/channels/MessageInput.tsx +302 -0
  161. package/src/components/channels/SearchInput.tsx +172 -0
  162. package/src/components/channels/SearchResults.tsx +336 -0
  163. package/src/components/channels/api.test.ts +1527 -0
  164. package/src/components/channels/api.ts +703 -0
  165. package/src/components/channels/index.ts +76 -0
  166. package/src/components/channels/mockApi.ts +344 -0
  167. package/src/components/channels/types.ts +566 -0
  168. package/src/components/hooks/index.ts +58 -0
  169. package/src/components/hooks/useAgentLogs.ts +504 -0
  170. package/src/components/hooks/useAgents.ts +127 -0
  171. package/src/components/hooks/useBroadcastDedup.test.ts +371 -0
  172. package/src/components/hooks/useBroadcastDedup.ts +86 -0
  173. package/src/components/hooks/useChannelAdmin.ts +329 -0
  174. package/src/components/hooks/useChannelBrowser.ts +239 -0
  175. package/src/components/hooks/useChannelCommands.ts +138 -0
  176. package/src/components/hooks/useChannels.ts +367 -0
  177. package/src/components/hooks/useDebounce.ts +29 -0
  178. package/src/components/hooks/useDirectMessage.test.ts +952 -0
  179. package/src/components/hooks/useDirectMessage.ts +141 -0
  180. package/src/components/hooks/useMessages.ts +310 -0
  181. package/src/components/hooks/useOrchestrator.test.ts +165 -0
  182. package/src/components/hooks/useOrchestrator.ts +424 -0
  183. package/src/components/hooks/usePinnedAgents.test.ts +356 -0
  184. package/src/components/hooks/usePinnedAgents.ts +140 -0
  185. package/src/components/hooks/usePresence.test.ts +245 -0
  186. package/src/components/hooks/usePresence.ts +377 -0
  187. package/src/components/hooks/useRecentRepos.ts +130 -0
  188. package/src/components/hooks/useSession.ts +209 -0
  189. package/src/components/hooks/useThread.ts +138 -0
  190. package/src/components/hooks/useTrajectory.ts +265 -0
  191. package/src/components/hooks/useWebSocket.ts +290 -0
  192. package/src/components/hooks/useWorkspaceMembers.ts +132 -0
  193. package/src/components/hooks/useWorkspaceRepos.ts +73 -0
  194. package/src/components/hooks/useWorkspaceStatus.ts +237 -0
  195. package/src/components/index.ts +81 -0
  196. package/src/components/layout/Header.tsx +311 -0
  197. package/src/components/layout/RepoContextHeader.tsx +361 -0
  198. package/src/components/layout/Sidebar.archive.test.tsx +126 -0
  199. package/src/components/layout/Sidebar.test.tsx +691 -0
  200. package/src/components/layout/Sidebar.tsx +900 -0
  201. package/src/components/layout/index.ts +7 -0
  202. package/src/components/settings/BillingSettingsPanel.tsx +564 -0
  203. package/src/components/settings/SettingsPage.tsx +683 -0
  204. package/src/components/settings/TeamSettingsPanel.tsx +560 -0
  205. package/src/components/settings/WorkspaceSettingsPanel.tsx +1368 -0
  206. package/src/components/settings/index.ts +11 -0
  207. package/src/components/settings/types.ts +79 -0
  208. package/src/components/utils/messageFormatting.test.tsx +331 -0
  209. package/src/components/utils/messageFormatting.tsx +597 -0
  210. package/src/index.ts +63 -0
  211. package/src/landing/AboutPage.tsx +77 -0
  212. package/src/landing/BlogContent.tsx +187 -0
  213. package/src/landing/BlogPage.tsx +47 -0
  214. package/src/landing/CareersPage.tsx +53 -0
  215. package/src/landing/ChangelogPage.tsx +33 -0
  216. package/src/landing/ContactPage.tsx +41 -0
  217. package/src/landing/DocsPage.tsx +43 -0
  218. package/src/landing/LandingPage.tsx +702 -0
  219. package/src/landing/PricingPage.tsx +549 -0
  220. package/src/landing/PrivacyPage.tsx +117 -0
  221. package/src/landing/SecurityPage.tsx +42 -0
  222. package/src/landing/StaticPage.tsx +165 -0
  223. package/src/landing/TermsPage.tsx +125 -0
  224. package/src/landing/blogData.ts +312 -0
  225. package/src/landing/index.ts +18 -0
  226. package/src/landing/styles.css +3673 -0
  227. package/src/lib/agent-merge.test.ts +43 -0
  228. package/src/lib/agent-merge.ts +35 -0
  229. package/src/lib/api.ts +1294 -0
  230. package/src/lib/cloudApi.ts +893 -0
  231. package/src/lib/colors.test.ts +175 -0
  232. package/src/lib/colors.ts +218 -0
  233. package/src/lib/config.ts +109 -0
  234. package/src/lib/hierarchy.ts +242 -0
  235. package/src/lib/stuckDetection.ts +142 -0
  236. package/src/lib/useUrlRouting.ts +190 -0
  237. package/src/types/index.ts +317 -0
  238. package/src/types/threading.ts +7 -0
  239. package/out/_next/static/chunks/285-dc644487a8d6500d.js +0 -1
  240. package/out/_next/static/css/4c58d9cf493aa626.css +0 -1
  241. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_buildManifest.js +0 -0
  242. /package/out/_next/static/{AqelRhy1vr2nBUcU0Iqcp → IxfA6RZu4trcsEMYlkQra}/_ssgManifest.js +0 -0
  243. /package/out/_next/static/chunks/{528-d375bc8b46912d2c.js → 528-f5f676996d613c25.js} +0 -0
  244. /package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/{page-a58308f43557b908.js → page-b194f207fbd91862.js} +0 -0
@@ -0,0 +1,703 @@
1
+ /**
2
+ * Channels API Service
3
+ *
4
+ * Channels are handled entirely by the daemon (not cloud).
5
+ * Real-time messaging uses the daemon's CHANNEL_* protocol while the HTTP API now reads from daemon storage.
6
+ *
7
+ * Cloud channels were removed because:
8
+ * - Daemon already has full channel protocol support (CHANNEL_JOIN, CHANNEL_MESSAGE, etc.)
9
+ * - Having two parallel implementations caused confusion
10
+ * - See trajectory traj_fnmapojrllau for architectural decision
11
+ */
12
+
13
+ import type {
14
+ Channel,
15
+ ChannelMember,
16
+ ChannelMessage,
17
+ ListChannelsResponse,
18
+ GetChannelResponse,
19
+ GetMessagesResponse,
20
+ CreateChannelRequest,
21
+ CreateChannelResponse,
22
+ SendMessageRequest,
23
+ SendMessageResponse,
24
+ SearchResponse,
25
+ } from './types';
26
+ import { getCsrfToken, getApiUrl, initializeWorkspaceId } from '../../lib/api';
27
+
28
+ /**
29
+ * Get current username from localStorage or return default
30
+ */
31
+ function getCurrentUsername(): string {
32
+ if (typeof window !== 'undefined') {
33
+ return localStorage.getItem('relay_username') || 'Dashboard';
34
+ }
35
+ return 'Dashboard';
36
+ }
37
+
38
+ /**
39
+ * Custom error class for API errors
40
+ */
41
+ export class ApiError extends Error {
42
+ constructor(
43
+ message: string,
44
+ public status: number,
45
+ public body?: string
46
+ ) {
47
+ super(message);
48
+ this.name = 'ApiError';
49
+ }
50
+ }
51
+
52
+ // =============================================================================
53
+ // Channel API Functions - daemon-backed with minimal placeholders
54
+ // =============================================================================
55
+
56
+ /**
57
+ * List all channels for a workspace
58
+ * Channels are workspace-scoped, not user-scoped
59
+ */
60
+ export async function listChannels(workspaceId?: string): Promise<ListChannelsResponse> {
61
+ // Ensure workspace ID is initialized for proper URL routing
62
+ initializeWorkspaceId();
63
+ const params = new URLSearchParams();
64
+ // workspaceId is required for cloud mode
65
+ if (workspaceId) {
66
+ params.set('workspaceId', workspaceId);
67
+ }
68
+ const url = getApiUrl(`/api/channels?${params.toString()}`);
69
+
70
+ try {
71
+ const res = await fetch(url, {
72
+ method: 'GET',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ credentials: 'include',
75
+ });
76
+
77
+ if (!res.ok) {
78
+ throw new ApiError('Failed to fetch channels', res.status);
79
+ }
80
+
81
+ const json = await res.json() as { channels?: Channel[]; archivedChannels?: Channel[] };
82
+ return {
83
+ channels: json.channels ?? [],
84
+ archivedChannels: json.archivedChannels ?? [],
85
+ };
86
+ } catch (error) {
87
+ if (error instanceof ApiError) throw error;
88
+ throw new ApiError('Network error fetching channels', 0);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Get channel details and members
94
+ */
95
+ export async function getChannel(
96
+ _workspaceId: string,
97
+ channelId: string
98
+ ): Promise<GetChannelResponse> {
99
+ // Minimal channel details until daemon exposes metadata
100
+ return {
101
+ channel: {
102
+ id: channelId,
103
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
104
+ visibility: 'public',
105
+ status: 'active',
106
+ createdAt: new Date().toISOString(),
107
+ createdBy: getCurrentUsername(),
108
+ memberCount: 0,
109
+ unreadCount: 0,
110
+ hasMentions: false,
111
+ isDm: channelId.startsWith('dm:'),
112
+ },
113
+ members: [],
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Get messages in a channel
119
+ */
120
+ export async function getMessages(
121
+ workspaceId: string,
122
+ channelId: string,
123
+ options?: { before?: string; limit?: number; threadId?: string }
124
+ ): Promise<GetMessagesResponse> {
125
+ // Activity feed is a virtual channel - don't fetch from API
126
+ // This is a safeguard in case the caller doesn't check
127
+ if (channelId === '__activity__') {
128
+ return { messages: [], hasMore: false, unread: { count: 0 } };
129
+ }
130
+
131
+ // Ensure workspace ID is initialized for proper URL routing
132
+ initializeWorkspaceId();
133
+ const params = new URLSearchParams();
134
+ if (options?.limit) params.set('limit', String(options.limit));
135
+ if (options?.before) {
136
+ // convert ISO to timestamp for server query
137
+ const ts = Date.parse(options.before);
138
+ if (!Number.isNaN(ts)) params.set('before', String(ts));
139
+ }
140
+ if (workspaceId) {
141
+ params.set('workspaceId', workspaceId);
142
+ }
143
+
144
+ const url = `/api/channels/${encodeURIComponent(channelId)}/messages${params.toString() ? `?${params.toString()}` : ''}`;
145
+ const res = await fetch(getApiUrl(url), {
146
+ method: 'GET',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ });
149
+
150
+ if (!res.ok) {
151
+ throw new ApiError('Failed to fetch channel messages', res.status);
152
+ }
153
+
154
+ const json = await res.json() as { messages: Array<ChannelMessage>; hasMore?: boolean };
155
+ return {
156
+ messages: json.messages ?? [],
157
+ hasMore: Boolean(json.hasMore),
158
+ unread: { count: 0 },
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Create a new channel
164
+ */
165
+ export async function createChannel(
166
+ workspaceId: string,
167
+ request: CreateChannelRequest
168
+ ): Promise<CreateChannelResponse> {
169
+ // Ensure workspace ID is initialized for proper URL routing
170
+ initializeWorkspaceId();
171
+
172
+ try {
173
+ const csrfToken = getCsrfToken();
174
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
175
+ if (csrfToken) {
176
+ headers['X-CSRF-Token'] = csrfToken;
177
+ }
178
+
179
+ const response = await fetch(getApiUrl('/api/channels'), {
180
+ method: 'POST',
181
+ headers,
182
+ credentials: 'include',
183
+ body: JSON.stringify({
184
+ name: request.name,
185
+ description: request.description,
186
+ isPrivate: request.visibility === 'private',
187
+ invites: request.members, // Array of strings or {id, type} objects
188
+ workspaceId,
189
+ }),
190
+ });
191
+
192
+ if (!response.ok) {
193
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
194
+ throw new ApiError(error.error || 'Failed to create channel', response.status);
195
+ }
196
+
197
+ const result = await response.json() as {
198
+ success: boolean;
199
+ channel: {
200
+ id: string;
201
+ name: string;
202
+ description?: string;
203
+ visibility: 'public' | 'private';
204
+ status: string;
205
+ createdAt: string;
206
+ createdBy: string;
207
+ };
208
+ };
209
+
210
+ return {
211
+ channel: {
212
+ id: result.channel.id,
213
+ name: result.channel.name,
214
+ description: result.channel.description,
215
+ visibility: result.channel.visibility,
216
+ status: result.channel.status as 'active' | 'archived',
217
+ createdAt: result.channel.createdAt,
218
+ createdBy: result.channel.createdBy,
219
+ memberCount: 1,
220
+ unreadCount: 0,
221
+ hasMentions: false,
222
+ isDm: false,
223
+ },
224
+ };
225
+ } catch (error) {
226
+ if (error instanceof ApiError) throw error;
227
+ throw new ApiError('Network error creating channel', 0);
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Send a message to a channel via daemon API
233
+ */
234
+ export async function sendMessage(
235
+ workspaceId: string,
236
+ channelId: string,
237
+ request: SendMessageRequest
238
+ ): Promise<SendMessageResponse> {
239
+ // Ensure workspace ID is initialized for proper URL routing
240
+ initializeWorkspaceId();
241
+ const username = getCurrentUsername();
242
+
243
+ try {
244
+ const response = await fetch(getApiUrl('/api/channels/message'), {
245
+ method: 'POST',
246
+ headers: { 'Content-Type': 'application/json' },
247
+ body: JSON.stringify({
248
+ username,
249
+ channel: channelId,
250
+ body: request.content,
251
+ thread: request.threadId,
252
+ workspaceId,
253
+ }),
254
+ });
255
+
256
+ if (!response.ok) {
257
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
258
+ throw new ApiError(error.error || 'Failed to send message', response.status);
259
+ }
260
+
261
+ // Return optimistic message for immediate UI update
262
+ // Real message will come via WebSocket
263
+ return {
264
+ message: {
265
+ id: `pending-${Date.now()}`,
266
+ channelId,
267
+ from: username,
268
+ fromEntityType: 'user',
269
+ content: request.content,
270
+ timestamp: new Date().toISOString(),
271
+ threadId: request.threadId,
272
+ isRead: true,
273
+ },
274
+ };
275
+ } catch (error) {
276
+ if (error instanceof ApiError) throw error;
277
+ throw new ApiError('Network error sending message', 0);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Join a channel via daemon API
283
+ */
284
+ export async function joinChannel(
285
+ workspaceId: string,
286
+ channelId: string
287
+ ): Promise<Channel> {
288
+ // Ensure workspace ID is initialized for proper URL routing
289
+ initializeWorkspaceId();
290
+ const username = getCurrentUsername();
291
+
292
+ try {
293
+ const response = await fetch(getApiUrl('/api/channels/join'), {
294
+ method: 'POST',
295
+ headers: { 'Content-Type': 'application/json' },
296
+ body: JSON.stringify({ username, channel: channelId, workspaceId }),
297
+ });
298
+
299
+ if (!response.ok) {
300
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
301
+ throw new ApiError(error.error || 'Failed to join channel', response.status);
302
+ }
303
+
304
+ return {
305
+ id: channelId,
306
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
307
+ visibility: 'public',
308
+ status: 'active',
309
+ createdAt: new Date().toISOString(),
310
+ createdBy: username,
311
+ memberCount: 1,
312
+ unreadCount: 0,
313
+ hasMentions: false,
314
+ isDm: channelId.startsWith('dm:'),
315
+ };
316
+ } catch (error) {
317
+ if (error instanceof ApiError) throw error;
318
+ throw new ApiError('Network error joining channel', 0);
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Leave a channel via daemon API
324
+ */
325
+ export async function leaveChannel(
326
+ workspaceId: string,
327
+ channelId: string
328
+ ): Promise<void> {
329
+ // Ensure workspace ID is initialized for proper URL routing
330
+ initializeWorkspaceId();
331
+ const username = getCurrentUsername();
332
+
333
+ try {
334
+ const response = await fetch(getApiUrl('/api/channels/leave'), {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ body: JSON.stringify({ username, channel: channelId, workspaceId }),
338
+ });
339
+
340
+ if (!response.ok) {
341
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
342
+ throw new ApiError(error.error || 'Failed to leave channel', response.status);
343
+ }
344
+ } catch (error) {
345
+ if (error instanceof ApiError) throw error;
346
+ throw new ApiError('Network error leaving channel', 0);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Archive a channel
352
+ */
353
+ export async function archiveChannel(
354
+ workspaceId: string,
355
+ channelId: string
356
+ ): Promise<Channel> {
357
+ // Ensure workspace ID is initialized for proper URL routing
358
+ initializeWorkspaceId();
359
+ const res = await fetch(getApiUrl('/api/channels/archive'), {
360
+ method: 'POST',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify({ channel: channelId, workspaceId }),
363
+ });
364
+ if (!res.ok) {
365
+ throw new ApiError('Failed to archive channel', res.status);
366
+ }
367
+ return {
368
+ id: channelId,
369
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
370
+ visibility: 'public',
371
+ status: 'archived',
372
+ createdAt: new Date().toISOString(),
373
+ createdBy: getCurrentUsername(),
374
+ memberCount: 0,
375
+ unreadCount: 0,
376
+ hasMentions: false,
377
+ isDm: channelId.startsWith('dm:'),
378
+ };
379
+ }
380
+
381
+ /**
382
+ * Unarchive a channel
383
+ */
384
+ export async function unarchiveChannel(
385
+ workspaceId: string,
386
+ channelId: string
387
+ ): Promise<Channel> {
388
+ // Ensure workspace ID is initialized for proper URL routing
389
+ initializeWorkspaceId();
390
+ const res = await fetch(getApiUrl('/api/channels/unarchive'), {
391
+ method: 'POST',
392
+ headers: { 'Content-Type': 'application/json' },
393
+ body: JSON.stringify({ channel: channelId, workspaceId }),
394
+ });
395
+ if (!res.ok) {
396
+ throw new ApiError('Failed to unarchive channel', res.status);
397
+ }
398
+ return {
399
+ id: channelId,
400
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
401
+ visibility: 'public',
402
+ status: 'active',
403
+ createdAt: new Date().toISOString(),
404
+ createdBy: getCurrentUsername(),
405
+ memberCount: 0,
406
+ unreadCount: 0,
407
+ hasMentions: false,
408
+ isDm: channelId.startsWith('dm:'),
409
+ };
410
+ }
411
+
412
+ /**
413
+ * Delete a channel (permanent)
414
+ */
415
+ export async function deleteChannel(
416
+ _workspaceId: string,
417
+ _channelId: string
418
+ ): Promise<void> {
419
+ // Daemon deletes automatically when empty; nothing to do client-side
420
+ return;
421
+ }
422
+
423
+ /**
424
+ * Mark messages as read
425
+ */
426
+ export async function markRead(
427
+ _workspaceId: string,
428
+ _channelId: string,
429
+ _upToMessageId?: string
430
+ ): Promise<void> {
431
+ // TODO: add mark-read to daemon; no-op for now
432
+ return;
433
+ }
434
+
435
+ /**
436
+ * Pin a message (no-op in daemon mode)
437
+ */
438
+ export async function pinMessage(
439
+ _workspaceId: string,
440
+ _channelId: string,
441
+ _messageId: string
442
+ ): Promise<void> {
443
+ // Pinning not supported in daemon mode
444
+ return;
445
+ }
446
+
447
+ /**
448
+ * Unpin a message (no-op in daemon mode)
449
+ */
450
+ export async function unpinMessage(
451
+ _workspaceId: string,
452
+ _channelId: string,
453
+ _messageId: string
454
+ ): Promise<void> {
455
+ // Unpinning not supported in daemon mode
456
+ return;
457
+ }
458
+
459
+ /**
460
+ * Get mention suggestions (online agents/users)
461
+ */
462
+ export async function getMentionSuggestions(
463
+ _workspaceId?: string
464
+ ): Promise<string[]> {
465
+ return ['lead', 'frontend', 'reviewer', 'ops', 'qa'];
466
+ }
467
+
468
+ /**
469
+ * Available member for channel invites
470
+ */
471
+ export interface AvailableMember {
472
+ id: string;
473
+ displayName: string;
474
+ type: 'user' | 'agent';
475
+ avatarUrl?: string;
476
+ status?: string;
477
+ }
478
+
479
+ /**
480
+ * Get available members for channel invites
481
+ * Returns workspace members (humans) and agents from linked daemons
482
+ */
483
+ export async function getAvailableMembers(
484
+ workspaceId?: string
485
+ ): Promise<{ members: AvailableMember[]; agents: AvailableMember[] }> {
486
+ // Ensure workspace ID is initialized for proper URL routing
487
+ initializeWorkspaceId();
488
+ const params = new URLSearchParams();
489
+ if (workspaceId) {
490
+ params.set('workspaceId', workspaceId);
491
+ }
492
+
493
+ try {
494
+ const url = getApiUrl(`/api/channels/available-members?${params.toString()}`);
495
+ const res = await fetch(url, {
496
+ method: 'GET',
497
+ headers: { 'Content-Type': 'application/json' },
498
+ credentials: 'include',
499
+ });
500
+
501
+ if (!res.ok) {
502
+ console.error('[ChannelsAPI] Failed to fetch available members:', res.status);
503
+ return { members: [], agents: [] };
504
+ }
505
+
506
+ const json = await res.json() as { members?: AvailableMember[]; agents?: AvailableMember[] };
507
+ return {
508
+ members: json.members ?? [],
509
+ agents: json.agents ?? [],
510
+ };
511
+ } catch (error) {
512
+ console.error('[ChannelsAPI] Error fetching available members:', error);
513
+ return { members: [], agents: [] };
514
+ }
515
+ }
516
+
517
+ // =============================================================================
518
+ // Search API Functions
519
+ // =============================================================================
520
+
521
+ /**
522
+ * Search messages (returns empty in daemon mode - search via relay)
523
+ */
524
+ export async function searchMessages(
525
+ _workspaceId: string,
526
+ query: string,
527
+ _options?: { channelId?: string; limit?: number; offset?: number }
528
+ ): Promise<SearchResponse> {
529
+ // Search not implemented in daemon mode
530
+ return {
531
+ results: [],
532
+ total: 0,
533
+ hasMore: false,
534
+ query,
535
+ };
536
+ }
537
+
538
+ /**
539
+ * Search within a specific channel
540
+ */
541
+ export async function searchChannel(
542
+ workspaceId: string,
543
+ channelId: string,
544
+ query: string,
545
+ options?: { limit?: number; offset?: number }
546
+ ): Promise<SearchResponse> {
547
+ return searchMessages(workspaceId, query, { ...options, channelId });
548
+ }
549
+
550
+ // =============================================================================
551
+ // Admin API Functions
552
+ // =============================================================================
553
+
554
+ /**
555
+ * Update channel settings
556
+ */
557
+ export async function updateChannel(
558
+ _workspaceId: string,
559
+ channelId: string,
560
+ updates: { name?: string; description?: string; isPrivate?: boolean }
561
+ ): Promise<Channel> {
562
+ const channel: Channel = {
563
+ id: channelId,
564
+ name: channelId.startsWith('#') ? channelId.slice(1) : channelId,
565
+ description: updates.description,
566
+ visibility: updates.isPrivate ? 'private' : 'public',
567
+ status: 'active',
568
+ createdAt: new Date().toISOString(),
569
+ createdBy: getCurrentUsername(),
570
+ memberCount: 0,
571
+ unreadCount: 0,
572
+ hasMentions: false,
573
+ isDm: channelId.startsWith('dm:'),
574
+ };
575
+ return {
576
+ ...channel,
577
+ name: updates.name ?? channel.name,
578
+ };
579
+ }
580
+
581
+ /**
582
+ * Add a member to a channel
583
+ */
584
+ export async function addMember(
585
+ _workspaceId: string,
586
+ _channelId: string,
587
+ request: { memberId: string; memberType: 'user' | 'agent'; role?: 'admin' | 'member' | 'read_only' }
588
+ ): Promise<ChannelMember> {
589
+ return {
590
+ id: request.memberId,
591
+ displayName: request.memberId,
592
+ entityType: request.memberType,
593
+ role: request.role === 'admin' ? 'admin' : 'member',
594
+ status: 'offline',
595
+ joinedAt: new Date().toISOString(),
596
+ };
597
+ }
598
+
599
+ /**
600
+ * Remove a member from a channel
601
+ */
602
+ export async function removeMember(
603
+ _workspaceId: string,
604
+ channelId: string,
605
+ memberId: string,
606
+ _memberType: 'user' | 'agent'
607
+ ): Promise<void> {
608
+ const url = getApiUrl('/api/channels/admin-remove');
609
+ const response = await fetch(url, {
610
+ method: 'POST',
611
+ headers: { 'Content-Type': 'application/json' },
612
+ body: JSON.stringify({
613
+ channel: channelId.startsWith('#') ? channelId : `#${channelId}`,
614
+ member: memberId,
615
+ }),
616
+ });
617
+ if (!response.ok) {
618
+ const error = await response.json().catch(() => ({}));
619
+ throw new ApiError(error.error || 'Failed to remove member', response.status);
620
+ }
621
+ }
622
+
623
+ /**
624
+ * Update a member's role
625
+ */
626
+ export async function updateMemberRole(
627
+ _workspaceId: string,
628
+ _channelId: string,
629
+ memberId: string,
630
+ request: { role: 'admin' | 'member' | 'read_only'; memberType: 'user' | 'agent' }
631
+ ): Promise<ChannelMember> {
632
+ return {
633
+ id: memberId,
634
+ displayName: memberId,
635
+ entityType: request.memberType,
636
+ role: request.role === 'admin' ? 'admin' : 'member',
637
+ status: 'offline',
638
+ joinedAt: new Date().toISOString(),
639
+ };
640
+ }
641
+
642
+ /**
643
+ * Get all members of a channel
644
+ */
645
+ export async function getChannelMembers(
646
+ _workspaceId: string,
647
+ channelId: string
648
+ ): Promise<ChannelMember[]> {
649
+ try {
650
+ const url = getApiUrl(`/api/channels/${encodeURIComponent(channelId)}/members`);
651
+ const response = await fetch(url, {
652
+ method: 'GET',
653
+ headers: { 'Content-Type': 'application/json' },
654
+ });
655
+ if (!response.ok) {
656
+ console.warn('[ChannelsAPI] Failed to get channel members:', response.statusText);
657
+ // Fall back to just returning current user
658
+ return [{
659
+ id: getCurrentUsername(),
660
+ displayName: getCurrentUsername(),
661
+ entityType: 'user',
662
+ role: 'owner',
663
+ status: 'online',
664
+ joinedAt: new Date().toISOString(),
665
+ }];
666
+ }
667
+ const data = await response.json();
668
+ return data.members || [];
669
+ } catch (error) {
670
+ console.error('[ChannelsAPI] Error getting channel members:', error);
671
+ // Fall back to just returning current user
672
+ return [{
673
+ id: getCurrentUsername(),
674
+ displayName: getCurrentUsername(),
675
+ entityType: 'user',
676
+ role: 'owner',
677
+ status: 'online',
678
+ joinedAt: new Date().toISOString(),
679
+ }];
680
+ }
681
+ }
682
+
683
+ // =============================================================================
684
+ // Feature Flag Utilities (kept for API compatibility)
685
+ // =============================================================================
686
+
687
+ /**
688
+ * Always returns true - channels now only use daemon/relay
689
+ */
690
+ export function isRealApiEnabled(): boolean {
691
+ return true;
692
+ }
693
+
694
+ /**
695
+ * No-op - API mode is fixed to daemon/local
696
+ */
697
+ export function setApiMode(_useReal: boolean): void {
698
+ console.log('[ChannelsAPI] Mode is fixed to daemon-based implementation');
699
+ }
700
+
701
+ export function getApiMode(): 'real' | 'mock' {
702
+ return 'real';
703
+ }