@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.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
- package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
- package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
- package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
- package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
- package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
- package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
- package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
- package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
- package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
- package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
- package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
- package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
- package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
- package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
- package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
- package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
- package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
- package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
- package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
- package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
- package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
- package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
- package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
- package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
- package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
- package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
- package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
- package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
- package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
- package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
- package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
- package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
- package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
- package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
- package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
- package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
- package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
- package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
- package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
- package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
- package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
- package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
- package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
- package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
- package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
- package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
- package/out/about.html +2 -2
- package/out/about.txt +2 -2
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +2 -2
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +2 -2
- package/out/changelog.html +2 -2
- package/out/changelog.txt +2 -2
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +2 -2
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +2 -2
- package/out/contact.html +2 -2
- package/out/contact.txt +2 -2
- package/out/dev/cli-tools.html +1 -0
- package/out/dev/cli-tools.txt +7 -0
- package/out/dev/log-viewer.html +23 -0
- package/out/dev/log-viewer.txt +7 -0
- package/out/docs.html +2 -2
- package/out/docs.txt +2 -2
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +2 -2
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +2 -2
- package/out/privacy.html +2 -2
- package/out/privacy.txt +2 -2
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +2 -2
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +2 -2
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +2 -2
- package/out/providers.html +1 -1
- package/out/providers.txt +2 -2
- package/out/security.html +2 -2
- package/out/security.txt +2 -2
- package/out/signup.html +2 -2
- package/out/signup.txt +2 -2
- package/out/terms.html +2 -2
- package/out/terms.txt +2 -2
- package/package.json +5 -1
- package/src/adapters/DashboardConfigProvider.tsx +56 -0
- package/src/adapters/cloudFetchAdapter.ts +278 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/types.ts +508 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
- package/src/app/app/onboarding/page.tsx +870 -170
- package/src/app/cloud/link/page.tsx +14 -6
- package/src/app/connect-repos/page.tsx +9 -3
- package/src/app/dev/cli-tools/page.tsx +130 -0
- package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
- package/src/app/dev/log-viewer/fixtures.ts +110 -0
- package/src/app/dev/log-viewer/page.tsx +288 -0
- package/src/app/history/page.tsx +28 -12
- package/src/app/page.tsx +1 -1
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
- package/src/components/AgentCard.tsx +4 -4
- package/src/components/AgentLogPreview.tsx +2 -38
- package/src/components/App.tsx +441 -2624
- package/src/components/CliToolHarness.test.tsx +83 -0
- package/src/components/CliToolHarness.tsx +292 -0
- package/src/components/CoordinatorPanel.tsx +13 -6
- package/src/components/LogViewer.tsx +2 -42
- package/src/components/ProviderAuthFlow.tsx +201 -81
- package/src/components/ProvisioningProgress.tsx +1 -1
- package/src/components/ReactionChips.tsx +2 -1
- package/src/components/SpawnModal.test.tsx +51 -18
- package/src/components/SpawnModal.tsx +175 -207
- package/src/components/TerminalProviderSetup.tsx +1 -1
- package/src/components/ThreadPanel.tsx +2 -0
- package/src/components/WorkspaceContext.tsx +7 -19
- package/src/components/XTermLogViewer.tsx +190 -27
- package/src/components/channels/ChannelMessageList.tsx +94 -4
- package/src/components/channels/ChannelViewV1.tsx +35 -11
- package/src/components/channels/api.ts +21 -20
- package/src/components/channels/types.ts +16 -0
- package/src/components/hooks/index.ts +0 -19
- package/src/components/hooks/useMessages.test.ts +80 -0
- package/src/components/hooks/useMessages.ts +13 -4
- package/src/components/hooks/useOrchestrator.ts +1 -1
- package/src/components/hooks/usePresence.ts +45 -6
- package/src/components/hooks/useThread.ts +83 -46
- package/src/components/hooks/useTrajectory.ts +62 -5
- package/src/components/hooks/useWebSocket.test.ts +358 -0
- package/src/components/hooks/useWebSocket.ts +243 -5
- package/src/components/index.ts +2 -14
- package/src/components/layout/Header.tsx +9 -15
- package/src/components/layout/Sidebar.tsx +1 -8
- package/src/components/settings/SettingsPage.tsx +108 -47
- package/src/components/settings/index.ts +0 -3
- package/src/landing/blogData.ts +1 -1
- package/src/lib/agent-merge.test.ts +2 -2
- package/src/lib/api.ts +8 -38
- package/src/lib/identity.test.ts +139 -0
- package/src/lib/identity.ts +48 -0
- package/src/lib/relaycastMessageAdapters.test.ts +182 -0
- package/src/lib/relaycastMessageAdapters.ts +105 -0
- package/src/lib/sanitize-logs.test.ts +227 -0
- package/src/lib/sanitize-logs.ts +202 -0
- package/src/providers/AgentProvider.tsx +799 -0
- package/src/providers/ChannelProvider.tsx +528 -0
- package/src/providers/CloudWorkspaceProvider.tsx +402 -0
- package/src/providers/MessageProvider.tsx +875 -0
- package/src/providers/RelayConfigProvider.tsx +94 -0
- package/src/providers/SendProvider.tsx +497 -0
- package/src/providers/SettingsProvider.tsx +247 -0
- package/src/providers/index.ts +26 -0
- package/src/types/index.ts +10 -10
- package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
- package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
- package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
- package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
- package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
- package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
- package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
- package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
- package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
- package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
- package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
- package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
- package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
- package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
- package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
- package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
- package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
- package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
- package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
- package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
- package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
- package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
- package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
- package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
- package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
- package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
- package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
- package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
- package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
- package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
- package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
- package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
- package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
- package/src/components/BillingResult.tsx +0 -447
- package/src/components/CloudSessionProvider.tsx +0 -130
- package/src/components/SessionExpiredModal.tsx +0 -128
- package/src/components/WorkspaceStatusIndicator.tsx +0 -396
- package/src/components/hooks/useSession.ts +0 -209
- package/src/components/hooks/useWorkspaceMembers.ts +0 -132
- package/src/components/hooks/useWorkspaceStatus.ts +0 -237
- package/src/components/settings/BillingSettingsPanel.tsx +0 -564
- package/src/components/settings/TeamSettingsPanel.tsx +0 -560
- package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
- package/src/lib/cloudApi.ts +0 -893
- /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message Provider
|
|
3
|
+
*
|
|
4
|
+
* Manages core message state, threads, DM conversations, presence,
|
|
5
|
+
* notifications, and WebSocket event handling. Composes ChannelProvider
|
|
6
|
+
* and SendProvider as children for channel CRUD and send operations.
|
|
7
|
+
*
|
|
8
|
+
* All values from the sub-providers are re-exported through useMessageContext
|
|
9
|
+
* for backward compatibility.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import React, { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
13
|
+
import type { Message } from '../types';
|
|
14
|
+
import type { HumanUser } from '../components/MentionAutocomplete';
|
|
15
|
+
import {
|
|
16
|
+
useDMs as useRelayDMs,
|
|
17
|
+
} from '@relaycast/react';
|
|
18
|
+
import { useMessages as useMessagesHook } from '../components/hooks/useMessages';
|
|
19
|
+
import { useThread } from '../components/hooks/useThread';
|
|
20
|
+
import { usePresence, type UserPresence } from '../components/hooks/usePresence';
|
|
21
|
+
import { useDirectMessage } from '../components/hooks/useDirectMessage';
|
|
22
|
+
import { useCloudWorkspace } from './CloudWorkspaceProvider';
|
|
23
|
+
import { useAgentContext } from './AgentProvider';
|
|
24
|
+
import { useRelayConfigStatus } from './RelayConfigProvider';
|
|
25
|
+
import { isDashboardVariant } from '../lib/identity';
|
|
26
|
+
import {
|
|
27
|
+
normalizeRelayDmMessageTargets,
|
|
28
|
+
} from '../lib/relaycastMessageAdapters';
|
|
29
|
+
import { playNotificationSound } from './SettingsProvider';
|
|
30
|
+
import { useSettings } from './SettingsProvider';
|
|
31
|
+
import {
|
|
32
|
+
type Channel,
|
|
33
|
+
type ChannelMember,
|
|
34
|
+
type ChannelMessage as ChannelApiMessage,
|
|
35
|
+
type UnreadState,
|
|
36
|
+
type CreateChannelRequest,
|
|
37
|
+
} from '../components/channels';
|
|
38
|
+
import type { DashboardData } from '../components/hooks/useWebSocket';
|
|
39
|
+
import { ChannelProvider, useChannelContext } from './ChannelProvider';
|
|
40
|
+
import { SendProvider, useSendContext } from './SendProvider';
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Constants
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
/** Special ID for the Activity feed (broadcasts) */
|
|
47
|
+
export const ACTIVITY_FEED_ID = '__activity__';
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Helper
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function isHumanSender(sender: string, agentNames: Set<string>, projectIdentity?: string | null): boolean {
|
|
54
|
+
return !isDashboardVariant(sender) &&
|
|
55
|
+
(projectIdentity ? sender !== projectIdentity : true) &&
|
|
56
|
+
sender !== '*' &&
|
|
57
|
+
!agentNames.has(sender.toLowerCase());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Types
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
interface MessageContextValue {
|
|
65
|
+
// Core message state
|
|
66
|
+
messages: Message[];
|
|
67
|
+
threadMessages: (threadId: string) => Message[];
|
|
68
|
+
currentChannel: string;
|
|
69
|
+
setCurrentChannel: (ch: string) => void;
|
|
70
|
+
currentThread: string | null;
|
|
71
|
+
setCurrentThread: (t: string | null) => void;
|
|
72
|
+
activeThreads: ReturnType<typeof useMessagesHook>['activeThreads'];
|
|
73
|
+
totalUnreadThreadCount: number;
|
|
74
|
+
sendMessage: (to: string, content: string, thread?: string, attachmentIds?: string[]) => Promise<boolean>;
|
|
75
|
+
isSending: boolean;
|
|
76
|
+
sendError: string | null;
|
|
77
|
+
|
|
78
|
+
// Thread hook (API-backed)
|
|
79
|
+
thread: ReturnType<typeof useThread>;
|
|
80
|
+
|
|
81
|
+
// Channel state (from ChannelProvider)
|
|
82
|
+
viewMode: 'local' | 'fleet' | 'channels';
|
|
83
|
+
setViewMode: React.Dispatch<React.SetStateAction<'local' | 'fleet' | 'channels'>>;
|
|
84
|
+
channelsList: Channel[];
|
|
85
|
+
archivedChannelsList: Channel[];
|
|
86
|
+
channelMessages: ChannelApiMessage[];
|
|
87
|
+
selectedChannelId: string | undefined;
|
|
88
|
+
setSelectedChannelId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
89
|
+
selectedChannel: Channel | undefined;
|
|
90
|
+
hasMoreMessages: boolean;
|
|
91
|
+
channelUnreadState: UnreadState | undefined;
|
|
92
|
+
isChannelsLoading: boolean;
|
|
93
|
+
effectiveChannelMessages: ChannelApiMessage[];
|
|
94
|
+
|
|
95
|
+
// Channel handlers (from ChannelProvider)
|
|
96
|
+
handleSelectChannel: (channel: Channel) => Promise<void>;
|
|
97
|
+
handleCreateChannel: () => void;
|
|
98
|
+
handleCreateChannelSubmit: (request: CreateChannelRequest) => Promise<void>;
|
|
99
|
+
handleInviteToChannel: (channel: Channel) => void;
|
|
100
|
+
handleInviteSubmit: (members: string[]) => Promise<void>;
|
|
101
|
+
handleJoinChannel: (channelId: string) => Promise<void>;
|
|
102
|
+
handleLeaveChannel: (channel: Channel) => Promise<void>;
|
|
103
|
+
handleShowMembers: () => Promise<void>;
|
|
104
|
+
handleRemoveMember: (memberId: string, memberType: 'user' | 'agent') => Promise<void>;
|
|
105
|
+
handleAddMember: (memberId: string, memberType: 'user' | 'agent', role: 'admin' | 'member' | 'read_only') => Promise<void>;
|
|
106
|
+
handleArchiveChannel: (channel: Channel) => Promise<void>;
|
|
107
|
+
handleUnarchiveChannel: (channel: Channel) => Promise<void>;
|
|
108
|
+
handleSendChannelMessage: (content: string, threadId?: string, attachmentIds?: string[]) => Promise<boolean>;
|
|
109
|
+
handleLoadMoreMessages: () => Promise<void>;
|
|
110
|
+
handleMarkChannelRead: (channelId: string) => void;
|
|
111
|
+
|
|
112
|
+
// Channel modals (from ChannelProvider)
|
|
113
|
+
isCreateChannelOpen: boolean;
|
|
114
|
+
setIsCreateChannelOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
115
|
+
isCreatingChannel: boolean;
|
|
116
|
+
isInviteChannelOpen: boolean;
|
|
117
|
+
setIsInviteChannelOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
118
|
+
inviteChannelTarget: Channel | null;
|
|
119
|
+
setInviteChannelTarget: React.Dispatch<React.SetStateAction<Channel | null>>;
|
|
120
|
+
isInvitingToChannel: boolean;
|
|
121
|
+
showMemberPanel: boolean;
|
|
122
|
+
setShowMemberPanel: React.Dispatch<React.SetStateAction<boolean>>;
|
|
123
|
+
channelMembers: ChannelMember[];
|
|
124
|
+
|
|
125
|
+
// DM state
|
|
126
|
+
currentHuman: import('../types').Agent | null;
|
|
127
|
+
selectedDmAgents: string[];
|
|
128
|
+
removedDmAgents: string[];
|
|
129
|
+
dedupedVisibleMessages: Message[];
|
|
130
|
+
dmParticipantAgents: string[];
|
|
131
|
+
dmSelectedAgentsByHuman: Record<string, string[]>;
|
|
132
|
+
handleDmAgentToggle: (agentName: string) => void;
|
|
133
|
+
handleDmSend: (content: string, attachmentIds?: string[]) => Promise<boolean>;
|
|
134
|
+
handleMainComposerSend: (content: string, attachmentIds?: string[]) => Promise<boolean>;
|
|
135
|
+
|
|
136
|
+
// Presence
|
|
137
|
+
onlineUsers: UserPresence[];
|
|
138
|
+
typingUsers: ReturnType<typeof usePresence>['typingUsers'];
|
|
139
|
+
sendTyping: ReturnType<typeof usePresence>['sendTyping'];
|
|
140
|
+
isPresenceConnected: boolean;
|
|
141
|
+
|
|
142
|
+
// Human users
|
|
143
|
+
humanUsers: HumanUser[];
|
|
144
|
+
humanUnreadCounts: Record<string, number>;
|
|
145
|
+
|
|
146
|
+
// Reactions (from SendProvider)
|
|
147
|
+
handleReaction: (messageId: string, emoji: string, hasReacted: boolean) => Promise<void>;
|
|
148
|
+
|
|
149
|
+
// DM tracking
|
|
150
|
+
markDmSeen: (username: string) => void;
|
|
151
|
+
|
|
152
|
+
// User profile
|
|
153
|
+
selectedUserProfile: UserPresence | null;
|
|
154
|
+
setSelectedUserProfile: React.Dispatch<React.SetStateAction<UserPresence | null>>;
|
|
155
|
+
pendingMention: string | undefined;
|
|
156
|
+
setPendingMention: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
157
|
+
|
|
158
|
+
// Notification state
|
|
159
|
+
hasUnreadMessages: boolean;
|
|
160
|
+
|
|
161
|
+
// WebSocket event handler ref (for passing to parent hooks)
|
|
162
|
+
handlePresenceEvent: (event: unknown) => void;
|
|
163
|
+
|
|
164
|
+
// Channel message map setter for external channel updates
|
|
165
|
+
setChannelsList: React.Dispatch<React.SetStateAction<Channel[]>>;
|
|
166
|
+
appendChannelMessage: (channelId: string, message: ChannelApiMessage, options?: { incrementUnread?: boolean }) => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Context
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
const MessageContext = createContext<MessageContextValue | null>(null);
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// Provider Props
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
export interface MessageProviderProps {
|
|
180
|
+
children: React.ReactNode;
|
|
181
|
+
data: DashboardData | null;
|
|
182
|
+
rawData: DashboardData | null;
|
|
183
|
+
enableReactions?: boolean;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Inner component that reads from ChannelProvider and SendProvider
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
interface MessageProviderInnerProps {
|
|
191
|
+
children: React.ReactNode;
|
|
192
|
+
data: DashboardData | null;
|
|
193
|
+
rawData: DashboardData | null;
|
|
194
|
+
enableReactions?: boolean;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function MessageProviderInner({ children, data, rawData: _rawData, enableReactions = false }: MessageProviderInnerProps) {
|
|
198
|
+
const { currentUser, effectiveActiveWorkspaceId, isWorkspaceFeaturesEnabled } = useCloudWorkspace();
|
|
199
|
+
const { agents, combinedAgents, addActivityEvent } = useAgentContext();
|
|
200
|
+
const { configured: relayConfigured, agentName: relayAgentName } = useRelayConfigStatus();
|
|
201
|
+
const { settings } = useSettings();
|
|
202
|
+
|
|
203
|
+
// Sub-provider contexts
|
|
204
|
+
const channelCtx = useChannelContext();
|
|
205
|
+
const sendCtx = useSendContext();
|
|
206
|
+
|
|
207
|
+
// In local mode, fetch the project name from the health endpoint so we never show "Dashboard".
|
|
208
|
+
const [localUsername, setLocalUsername] = useState<string | null>(
|
|
209
|
+
typeof window !== 'undefined' ? localStorage.getItem('relay_username') : null
|
|
210
|
+
);
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
const stored = typeof window !== 'undefined' ? localStorage.getItem('relay_username') : null;
|
|
213
|
+
if (stored && stored !== localUsername) {
|
|
214
|
+
setLocalUsername(stored);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!localUsername) {
|
|
218
|
+
fetch('/api/health')
|
|
219
|
+
.then((res) => res.ok ? res.json() : null)
|
|
220
|
+
.then((data) => {
|
|
221
|
+
if (data?.projectName) {
|
|
222
|
+
localStorage.setItem('relay_username', data.projectName);
|
|
223
|
+
setLocalUsername(data.projectName);
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
.catch(() => {});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// View mode
|
|
231
|
+
const [viewMode, setViewMode] = useState<'local' | 'fleet' | 'channels'>(
|
|
232
|
+
isWorkspaceFeaturesEnabled ? 'local' : 'channels'
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
// DM state
|
|
236
|
+
const [dmSelectedAgentsByHuman, setDmSelectedAgentsByHuman] = useState<Record<string, string[]>>({});
|
|
237
|
+
const [dmRemovedAgentsByHuman, setDmRemovedAgentsByHuman] = useState<Record<string, string[]>>({});
|
|
238
|
+
const [dmSeenAt, setDmSeenAt] = useState<Map<string, number>>(new Map());
|
|
239
|
+
|
|
240
|
+
// User profile panel state
|
|
241
|
+
const [selectedUserProfile, setSelectedUserProfile] = useState<UserPresence | null>(null);
|
|
242
|
+
const [pendingMention, setPendingMention] = useState<string | undefined>();
|
|
243
|
+
|
|
244
|
+
// Mobile unread tracking
|
|
245
|
+
const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
|
|
246
|
+
const lastSeenMessageCountRef = useRef<number>(0);
|
|
247
|
+
const lastNotifiedMessageIdRef = useRef<string | null>(null);
|
|
248
|
+
const relayRealtimeEnabledRef = useRef(false);
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// Presence event handler (used by usePresence and the WebSocket onEvent)
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
const handlePresenceEvent = useCallback((event: any) => {
|
|
255
|
+
if (event?.type === 'presence_join' && event.user) {
|
|
256
|
+
const user = event.user;
|
|
257
|
+
if (user.username !== currentUser?.displayName) {
|
|
258
|
+
addActivityEvent({
|
|
259
|
+
type: 'user_joined',
|
|
260
|
+
actor: user.username,
|
|
261
|
+
actorAvatarUrl: user.avatarUrl,
|
|
262
|
+
actorType: 'user',
|
|
263
|
+
title: 'came online',
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
} else if (event?.type === 'presence_leave' && event.username) {
|
|
267
|
+
if (event.username !== currentUser?.displayName) {
|
|
268
|
+
addActivityEvent({
|
|
269
|
+
type: 'user_left',
|
|
270
|
+
actor: event.username,
|
|
271
|
+
actorType: 'user',
|
|
272
|
+
title: 'went offline',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
} else if (event?.type === 'agent_spawned' && event.agent) {
|
|
276
|
+
addActivityEvent({
|
|
277
|
+
type: 'agent_spawned',
|
|
278
|
+
actor: event.agent.name || event.agent,
|
|
279
|
+
actorType: 'agent',
|
|
280
|
+
title: 'was spawned',
|
|
281
|
+
description: event.task,
|
|
282
|
+
metadata: { cli: event.cli, task: event.task, spawnedBy: event.spawnedBy },
|
|
283
|
+
});
|
|
284
|
+
} else if (event?.type === 'agent_released' && event.agent) {
|
|
285
|
+
addActivityEvent({
|
|
286
|
+
type: 'agent_released',
|
|
287
|
+
actor: event.agent.name || event.agent,
|
|
288
|
+
actorType: 'agent',
|
|
289
|
+
title: 'was released',
|
|
290
|
+
metadata: { releasedBy: event.releasedBy },
|
|
291
|
+
});
|
|
292
|
+
} else if (event?.type === 'channel_created') {
|
|
293
|
+
const newChannel = event.channel;
|
|
294
|
+
if (!newChannel || !newChannel.id) return;
|
|
295
|
+
|
|
296
|
+
channelCtx.setChannelsList(prev => {
|
|
297
|
+
if (prev.some(c => c.id === newChannel.id)) return prev;
|
|
298
|
+
|
|
299
|
+
const channel: Channel = {
|
|
300
|
+
id: newChannel.id,
|
|
301
|
+
name: newChannel.name || newChannel.id,
|
|
302
|
+
description: newChannel.description,
|
|
303
|
+
visibility: newChannel.visibility || 'public',
|
|
304
|
+
status: newChannel.status || 'active',
|
|
305
|
+
createdAt: newChannel.createdAt || new Date().toISOString(),
|
|
306
|
+
createdBy: newChannel.createdBy || 'unknown',
|
|
307
|
+
memberCount: newChannel.memberCount || 1,
|
|
308
|
+
unreadCount: newChannel.unreadCount || 0,
|
|
309
|
+
hasMentions: newChannel.hasMentions || false,
|
|
310
|
+
isDm: newChannel.isDm || false,
|
|
311
|
+
};
|
|
312
|
+
console.log('[MessageProvider] Channel created via WebSocket:', channel.id);
|
|
313
|
+
return [...prev, channel];
|
|
314
|
+
});
|
|
315
|
+
} else if (event?.type === 'channel_message') {
|
|
316
|
+
if (relayRealtimeEnabledRef.current) return;
|
|
317
|
+
const channelId = event.channel as string | undefined;
|
|
318
|
+
if (!channelId) return;
|
|
319
|
+
const sender = event.from || 'unknown';
|
|
320
|
+
const fromEntityType = event.fromEntityType || (currentUser?.displayName && sender === currentUser.displayName ? 'user' : 'agent');
|
|
321
|
+
const msg: ChannelApiMessage = {
|
|
322
|
+
id: event.id ?? `ws-${Date.now()}`,
|
|
323
|
+
channelId,
|
|
324
|
+
from: sender,
|
|
325
|
+
fromEntityType,
|
|
326
|
+
fromAvatarUrl: event.fromAvatarUrl,
|
|
327
|
+
content: event.body ?? '',
|
|
328
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
329
|
+
threadId: event.thread,
|
|
330
|
+
isRead: channelCtx.selectedChannelId === channelId,
|
|
331
|
+
};
|
|
332
|
+
sendCtx.appendChannelMessage(channelId, msg, { incrementUnread: channelCtx.selectedChannelId !== channelId });
|
|
333
|
+
} else if (event?.type === 'direct_message') {
|
|
334
|
+
if (relayRealtimeEnabledRef.current) return;
|
|
335
|
+
const sender = event.from || 'unknown';
|
|
336
|
+
const recipient = currentUser?.displayName || event.targetUser || relayAgentName || 'Dashboard';
|
|
337
|
+
|
|
338
|
+
const participants = [sender, recipient].sort();
|
|
339
|
+
const dmChannelId = `dm:${participants.join(':')}`;
|
|
340
|
+
|
|
341
|
+
const fromEntityType = event.fromEntityType || 'agent';
|
|
342
|
+
const msg: ChannelApiMessage = {
|
|
343
|
+
id: event.id ?? `dm-${Date.now()}`,
|
|
344
|
+
channelId: dmChannelId,
|
|
345
|
+
from: sender,
|
|
346
|
+
fromEntityType,
|
|
347
|
+
fromAvatarUrl: event.fromAvatarUrl,
|
|
348
|
+
content: event.body ?? '',
|
|
349
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
350
|
+
threadId: event.thread,
|
|
351
|
+
isRead: channelCtx.selectedChannelId === dmChannelId,
|
|
352
|
+
};
|
|
353
|
+
sendCtx.appendChannelMessage(dmChannelId, msg, { incrementUnread: channelCtx.selectedChannelId !== dmChannelId });
|
|
354
|
+
}
|
|
355
|
+
}, [addActivityEvent, sendCtx.appendChannelMessage, currentUser?.displayName, channelCtx.selectedChannelId, channelCtx.setChannelsList, relayAgentName]);
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Presence
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
|
|
361
|
+
const presenceUser = useMemo(() =>
|
|
362
|
+
currentUser
|
|
363
|
+
? { username: currentUser.displayName, avatarUrl: currentUser.avatarUrl }
|
|
364
|
+
: undefined,
|
|
365
|
+
[currentUser?.displayName, currentUser?.avatarUrl]
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
const { onlineUsers: allOnlineUsers, typingUsers, sendTyping, isConnected: isPresenceConnected } = usePresence({
|
|
369
|
+
currentUser: presenceUser,
|
|
370
|
+
onEvent: handlePresenceEvent,
|
|
371
|
+
workspaceId: effectiveActiveWorkspaceId ?? undefined,
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
const onlineUsers = allOnlineUsers;
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Relay DMs and message normalization
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
|
|
380
|
+
const relayDMsState = useRelayDMs();
|
|
381
|
+
const normalizedRelayMessages = useMemo(() => {
|
|
382
|
+
const sourceMessages = data?.messages ?? [];
|
|
383
|
+
if (!relayConfigured || relayDMsState.conversations.length === 0) {
|
|
384
|
+
return sourceMessages;
|
|
385
|
+
}
|
|
386
|
+
return normalizeRelayDmMessageTargets(sourceMessages, relayDMsState.conversations);
|
|
387
|
+
}, [data?.messages, relayConfigured, relayDMsState.conversations]);
|
|
388
|
+
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Core message hook
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
const {
|
|
394
|
+
messages,
|
|
395
|
+
threadMessages,
|
|
396
|
+
currentChannel,
|
|
397
|
+
setCurrentChannel,
|
|
398
|
+
currentThread,
|
|
399
|
+
setCurrentThread,
|
|
400
|
+
activeThreads,
|
|
401
|
+
totalUnreadThreadCount,
|
|
402
|
+
sendMessage,
|
|
403
|
+
isSending,
|
|
404
|
+
sendError,
|
|
405
|
+
} = useMessagesHook({
|
|
406
|
+
messages: normalizedRelayMessages,
|
|
407
|
+
senderName: currentUser?.displayName || localUsername || undefined,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// ---------------------------------------------------------------------------
|
|
411
|
+
// Thread data
|
|
412
|
+
// ---------------------------------------------------------------------------
|
|
413
|
+
|
|
414
|
+
const thread = useThread({
|
|
415
|
+
threadId: viewMode === 'channels'
|
|
416
|
+
? (relayConfigured ? currentThread : null)
|
|
417
|
+
: currentThread,
|
|
418
|
+
fallbackMessages: messages,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
// DM state
|
|
423
|
+
// ---------------------------------------------------------------------------
|
|
424
|
+
|
|
425
|
+
const currentHuman = useMemo(() => {
|
|
426
|
+
if (!currentChannel) return null;
|
|
427
|
+
return combinedAgents.find(
|
|
428
|
+
(a) => a.isHuman && a.name.toLowerCase() === currentChannel.toLowerCase()
|
|
429
|
+
) || null;
|
|
430
|
+
}, [combinedAgents, currentChannel]);
|
|
431
|
+
|
|
432
|
+
const selectedDmAgents = useMemo(
|
|
433
|
+
() => (currentHuman ? dmSelectedAgentsByHuman[currentHuman.name] ?? [] : []),
|
|
434
|
+
[currentHuman, dmSelectedAgentsByHuman]
|
|
435
|
+
);
|
|
436
|
+
const removedDmAgents = useMemo(
|
|
437
|
+
() => (currentHuman ? dmRemovedAgentsByHuman[currentHuman.name] ?? [] : []),
|
|
438
|
+
[currentHuman, dmRemovedAgentsByHuman]
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const { visibleMessages: dedupedVisibleMessages, participantAgents: dmParticipantAgents } = useDirectMessage({
|
|
442
|
+
currentHuman,
|
|
443
|
+
currentUserName: currentUser?.displayName ?? null,
|
|
444
|
+
messages,
|
|
445
|
+
agents,
|
|
446
|
+
selectedDmAgents,
|
|
447
|
+
removedDmAgents,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
relayRealtimeEnabledRef.current = relayConfigured;
|
|
452
|
+
}, [relayConfigured]);
|
|
453
|
+
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Human users extraction
|
|
456
|
+
// ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
const humanUsers = useMemo((): HumanUser[] => {
|
|
459
|
+
const agentNames = new Set(agents.filter((a) => !a.isHuman).map((a) => a.name.toLowerCase()));
|
|
460
|
+
const seenUsers = new Map<string, HumanUser>();
|
|
461
|
+
|
|
462
|
+
if (currentUser) {
|
|
463
|
+
seenUsers.set(currentUser.displayName.toLowerCase(), {
|
|
464
|
+
username: currentUser.displayName,
|
|
465
|
+
avatarUrl: currentUser.avatarUrl,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (relayConfigured && relayDMsState.conversations.length > 0) {
|
|
470
|
+
const currentUserName = currentUser?.displayName.toLowerCase();
|
|
471
|
+
for (const conversation of relayDMsState.conversations) {
|
|
472
|
+
for (const participant of conversation.participants) {
|
|
473
|
+
const name = typeof participant === 'string' ? participant : participant.agentName;
|
|
474
|
+
if (!name) continue;
|
|
475
|
+
const lowered = name.toLowerCase();
|
|
476
|
+
if (currentUserName && lowered === currentUserName) continue;
|
|
477
|
+
if (agentNames.has(lowered)) continue;
|
|
478
|
+
if (!seenUsers.has(lowered)) {
|
|
479
|
+
seenUsers.set(lowered, { username: name });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const msg of normalizedRelayMessages) {
|
|
486
|
+
const sender = msg.from;
|
|
487
|
+
if (sender && isHumanSender(sender, agentNames, relayAgentName) && !seenUsers.has(sender.toLowerCase())) {
|
|
488
|
+
seenUsers.set(sender.toLowerCase(), { username: sender });
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return Array.from(seenUsers.values());
|
|
493
|
+
}, [normalizedRelayMessages, agents, currentUser, relayDMsState.conversations, relayConfigured, relayAgentName]);
|
|
494
|
+
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
// Human unread counts
|
|
497
|
+
// ---------------------------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
const humanUnreadCounts = useMemo(() => {
|
|
500
|
+
if (!currentUser) return {};
|
|
501
|
+
|
|
502
|
+
if (relayConfigured && relayDMsState.conversations.length > 0) {
|
|
503
|
+
const counts: Record<string, number> = {};
|
|
504
|
+
const currentUserName = currentUser.displayName.toLowerCase();
|
|
505
|
+
const agentNames = new Set(agents.filter((a) => !a.isHuman).map((a) => a.name.toLowerCase()));
|
|
506
|
+
|
|
507
|
+
for (const conversation of relayDMsState.conversations) {
|
|
508
|
+
if (!conversation.unreadCount) continue;
|
|
509
|
+
|
|
510
|
+
const match = conversation.participants.find((p) => {
|
|
511
|
+
const name = typeof p === 'string' ? p : p.agentName;
|
|
512
|
+
if (!name) return false;
|
|
513
|
+
const lowered = name.toLowerCase();
|
|
514
|
+
return lowered !== currentUserName && !agentNames.has(lowered);
|
|
515
|
+
});
|
|
516
|
+
const participantName = match ? (typeof match === 'string' ? match : match.agentName) : null;
|
|
517
|
+
|
|
518
|
+
if (participantName) {
|
|
519
|
+
counts[participantName] = (counts[participantName] || 0) + conversation.unreadCount;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return counts;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const counts: Record<string, number> = {};
|
|
527
|
+
const humanNameSet = new Set(
|
|
528
|
+
combinedAgents.filter((a) => a.isHuman).map((a) => a.name.toLowerCase())
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
for (const msg of normalizedRelayMessages) {
|
|
532
|
+
const sender = msg.from;
|
|
533
|
+
const recipient = msg.to;
|
|
534
|
+
if (!sender || !recipient) continue;
|
|
535
|
+
|
|
536
|
+
const isToCurrentUser = recipient === currentUser.displayName;
|
|
537
|
+
const senderIsHuman = humanNameSet.has(sender.toLowerCase());
|
|
538
|
+
if (!isToCurrentUser || !senderIsHuman) continue;
|
|
539
|
+
|
|
540
|
+
const seenAt = dmSeenAt.get(sender.toLowerCase()) ?? 0;
|
|
541
|
+
const ts = new Date(msg.timestamp).getTime();
|
|
542
|
+
if (ts > seenAt) {
|
|
543
|
+
counts[sender] = (counts[sender] || 0) + 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return counts;
|
|
548
|
+
}, [combinedAgents, currentUser, normalizedRelayMessages, dmSeenAt, relayDMsState.conversations, agents, relayConfigured]);
|
|
549
|
+
|
|
550
|
+
const markDmSeen = useCallback((username: string) => {
|
|
551
|
+
setDmSeenAt((prev) => {
|
|
552
|
+
const next = new Map(prev);
|
|
553
|
+
next.set(username.toLowerCase(), Date.now());
|
|
554
|
+
return next;
|
|
555
|
+
});
|
|
556
|
+
}, []);
|
|
557
|
+
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
// DM handlers
|
|
560
|
+
// ---------------------------------------------------------------------------
|
|
561
|
+
|
|
562
|
+
const handleDmAgentToggle = useCallback((agentName: string) => {
|
|
563
|
+
if (!currentHuman) return;
|
|
564
|
+
const humanName = currentHuman.name;
|
|
565
|
+
const isSelected = (dmSelectedAgentsByHuman[humanName] ?? []).includes(agentName);
|
|
566
|
+
|
|
567
|
+
setDmSelectedAgentsByHuman((prev) => {
|
|
568
|
+
const currentList = prev[humanName] ?? [];
|
|
569
|
+
const nextList = isSelected
|
|
570
|
+
? currentList.filter((a) => a !== agentName)
|
|
571
|
+
: [...currentList, agentName];
|
|
572
|
+
return { ...prev, [humanName]: nextList };
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
setDmRemovedAgentsByHuman((prev) => {
|
|
576
|
+
const currentList = prev[humanName] ?? [];
|
|
577
|
+
if (isSelected) {
|
|
578
|
+
return currentList.includes(agentName)
|
|
579
|
+
? prev
|
|
580
|
+
: { ...prev, [humanName]: [...currentList, agentName] };
|
|
581
|
+
}
|
|
582
|
+
return { ...prev, [humanName]: currentList.filter((a) => a !== agentName) };
|
|
583
|
+
});
|
|
584
|
+
}, [currentHuman, dmSelectedAgentsByHuman]);
|
|
585
|
+
|
|
586
|
+
const handleDmSend = useCallback(async (content: string, attachmentIds?: string[]): Promise<boolean> => {
|
|
587
|
+
if (!currentHuman) return false;
|
|
588
|
+
const humanName = currentHuman.name;
|
|
589
|
+
|
|
590
|
+
await sendMessage(humanName, content, undefined, attachmentIds);
|
|
591
|
+
|
|
592
|
+
if (selectedDmAgents.length > 0) {
|
|
593
|
+
for (const agent of selectedDmAgents) {
|
|
594
|
+
await sendMessage(agent, content, undefined, attachmentIds);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return true;
|
|
599
|
+
}, [currentHuman, selectedDmAgents, sendMessage]);
|
|
600
|
+
|
|
601
|
+
const handleMainComposerSend = useCallback(
|
|
602
|
+
async (content: string, attachmentIds?: string[]) => {
|
|
603
|
+
if (currentHuman) {
|
|
604
|
+
return handleDmSend(content, attachmentIds);
|
|
605
|
+
}
|
|
606
|
+
return sendMessage(currentChannel, content, undefined, attachmentIds);
|
|
607
|
+
},
|
|
608
|
+
[currentChannel, currentHuman, handleDmSend, sendMessage]
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
// ---------------------------------------------------------------------------
|
|
612
|
+
// Browser notifications and sounds
|
|
613
|
+
// ---------------------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
useEffect(() => {
|
|
616
|
+
const msgs = normalizedRelayMessages;
|
|
617
|
+
if (!msgs || msgs.length === 0) {
|
|
618
|
+
lastNotifiedMessageIdRef.current = null;
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const latestMessage = msgs[msgs.length - 1];
|
|
623
|
+
|
|
624
|
+
if (!settings.notifications.enabled) {
|
|
625
|
+
lastNotifiedMessageIdRef.current = latestMessage?.id ?? null;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (!lastNotifiedMessageIdRef.current) {
|
|
630
|
+
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const lastNotifiedIndex = msgs.findIndex((message) => (
|
|
635
|
+
message.id === lastNotifiedMessageIdRef.current
|
|
636
|
+
));
|
|
637
|
+
|
|
638
|
+
if (lastNotifiedIndex === -1) {
|
|
639
|
+
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const newMessages = msgs.slice(lastNotifiedIndex + 1);
|
|
644
|
+
if (newMessages.length === 0) return;
|
|
645
|
+
|
|
646
|
+
lastNotifiedMessageIdRef.current = latestMessage.id;
|
|
647
|
+
|
|
648
|
+
const isFromCurrentUser = (message: Message) =>
|
|
649
|
+
message.from === 'Dashboard' ||
|
|
650
|
+
message.from === relayAgentName ||
|
|
651
|
+
(currentUser && message.from === currentUser.displayName);
|
|
652
|
+
|
|
653
|
+
const isMessageInCurrentChannel = (message: Message) => {
|
|
654
|
+
return message.from === currentChannel || message.to === currentChannel;
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
const shouldNotifyForMessage = (message: Message) => {
|
|
658
|
+
if (isFromCurrentUser(message)) return false;
|
|
659
|
+
if (settings.notifications.mentionsOnly && currentUser?.displayName) {
|
|
660
|
+
if (!message.content.includes(`@${currentUser.displayName}`)) return false;
|
|
661
|
+
}
|
|
662
|
+
const isActive = typeof document !== 'undefined' ? !document.hidden : false;
|
|
663
|
+
if (isActive && isMessageInCurrentChannel(message)) return false;
|
|
664
|
+
return true;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
let shouldPlaySound = false;
|
|
668
|
+
|
|
669
|
+
for (const message of newMessages) {
|
|
670
|
+
if (!shouldNotifyForMessage(message)) continue;
|
|
671
|
+
|
|
672
|
+
if (settings.notifications.desktop && typeof window !== 'undefined' && 'Notification' in window) {
|
|
673
|
+
if (Notification.permission === 'granted') {
|
|
674
|
+
const channelLabel = message.to;
|
|
675
|
+
const body = message.content.split('\n')[0].slice(0, 160);
|
|
676
|
+
const notification = new Notification(`${message.from} -> ${channelLabel}`, { body });
|
|
677
|
+
notification.onclick = () => {
|
|
678
|
+
window.focus();
|
|
679
|
+
setCurrentChannel(message.from);
|
|
680
|
+
notification.close();
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (settings.notifications.sound) {
|
|
686
|
+
shouldPlaySound = true;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (shouldPlaySound) {
|
|
691
|
+
playNotificationSound();
|
|
692
|
+
}
|
|
693
|
+
}, [normalizedRelayMessages, settings.notifications, currentChannel, currentUser, setCurrentChannel, relayAgentName]);
|
|
694
|
+
|
|
695
|
+
// Mobile unread tracking
|
|
696
|
+
useEffect(() => {
|
|
697
|
+
lastSeenMessageCountRef.current = messages.length;
|
|
698
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
699
|
+
}, []);
|
|
700
|
+
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Compose context value (merging sub-provider values for backward compat)
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
|
|
705
|
+
const value = useMemo<MessageContextValue>(() => ({
|
|
706
|
+
// Core message state
|
|
707
|
+
messages, threadMessages, currentChannel, setCurrentChannel,
|
|
708
|
+
currentThread, setCurrentThread, activeThreads, totalUnreadThreadCount,
|
|
709
|
+
sendMessage, isSending, sendError, thread,
|
|
710
|
+
|
|
711
|
+
// View mode
|
|
712
|
+
viewMode, setViewMode,
|
|
713
|
+
|
|
714
|
+
// Channel state (from ChannelProvider)
|
|
715
|
+
channelsList: channelCtx.channelsList,
|
|
716
|
+
archivedChannelsList: channelCtx.archivedChannelsList,
|
|
717
|
+
selectedChannelId: channelCtx.selectedChannelId,
|
|
718
|
+
setSelectedChannelId: channelCtx.setSelectedChannelId,
|
|
719
|
+
selectedChannel: channelCtx.selectedChannel,
|
|
720
|
+
isChannelsLoading: channelCtx.isChannelsLoading,
|
|
721
|
+
|
|
722
|
+
// Channel message state (from SendProvider)
|
|
723
|
+
channelMessages: sendCtx.channelMessages,
|
|
724
|
+
hasMoreMessages: sendCtx.hasMoreMessages,
|
|
725
|
+
channelUnreadState: sendCtx.channelUnreadState,
|
|
726
|
+
effectiveChannelMessages: sendCtx.effectiveChannelMessages,
|
|
727
|
+
|
|
728
|
+
// Channel handlers (from ChannelProvider)
|
|
729
|
+
handleSelectChannel: channelCtx.handleSelectChannel,
|
|
730
|
+
handleCreateChannel: channelCtx.handleCreateChannel,
|
|
731
|
+
handleCreateChannelSubmit: channelCtx.handleCreateChannelSubmit,
|
|
732
|
+
handleInviteToChannel: channelCtx.handleInviteToChannel,
|
|
733
|
+
handleInviteSubmit: channelCtx.handleInviteSubmit,
|
|
734
|
+
handleJoinChannel: channelCtx.handleJoinChannel,
|
|
735
|
+
handleLeaveChannel: channelCtx.handleLeaveChannel,
|
|
736
|
+
handleShowMembers: channelCtx.handleShowMembers,
|
|
737
|
+
handleRemoveMember: channelCtx.handleRemoveMember,
|
|
738
|
+
handleAddMember: channelCtx.handleAddMember,
|
|
739
|
+
handleArchiveChannel: channelCtx.handleArchiveChannel,
|
|
740
|
+
handleUnarchiveChannel: channelCtx.handleUnarchiveChannel,
|
|
741
|
+
|
|
742
|
+
// Send handlers (from SendProvider)
|
|
743
|
+
handleSendChannelMessage: sendCtx.handleSendChannelMessage,
|
|
744
|
+
handleLoadMoreMessages: sendCtx.handleLoadMoreMessages,
|
|
745
|
+
handleMarkChannelRead: sendCtx.handleMarkChannelRead,
|
|
746
|
+
handleReaction: sendCtx.handleReaction,
|
|
747
|
+
|
|
748
|
+
// Channel modals (from ChannelProvider)
|
|
749
|
+
isCreateChannelOpen: channelCtx.isCreateChannelOpen,
|
|
750
|
+
setIsCreateChannelOpen: channelCtx.setIsCreateChannelOpen,
|
|
751
|
+
isCreatingChannel: channelCtx.isCreatingChannel,
|
|
752
|
+
isInviteChannelOpen: channelCtx.isInviteChannelOpen,
|
|
753
|
+
setIsInviteChannelOpen: channelCtx.setIsInviteChannelOpen,
|
|
754
|
+
inviteChannelTarget: channelCtx.inviteChannelTarget,
|
|
755
|
+
setInviteChannelTarget: channelCtx.setInviteChannelTarget,
|
|
756
|
+
isInvitingToChannel: channelCtx.isInvitingToChannel,
|
|
757
|
+
showMemberPanel: channelCtx.showMemberPanel,
|
|
758
|
+
setShowMemberPanel: channelCtx.setShowMemberPanel,
|
|
759
|
+
channelMembers: channelCtx.channelMembers,
|
|
760
|
+
|
|
761
|
+
// DM state
|
|
762
|
+
currentHuman, selectedDmAgents, removedDmAgents,
|
|
763
|
+
dedupedVisibleMessages, dmParticipantAgents,
|
|
764
|
+
dmSelectedAgentsByHuman, handleDmAgentToggle,
|
|
765
|
+
handleDmSend, handleMainComposerSend,
|
|
766
|
+
|
|
767
|
+
// Presence
|
|
768
|
+
onlineUsers, typingUsers, sendTyping, isPresenceConnected,
|
|
769
|
+
|
|
770
|
+
// Human users
|
|
771
|
+
humanUsers, humanUnreadCounts,
|
|
772
|
+
|
|
773
|
+
// DM tracking
|
|
774
|
+
markDmSeen,
|
|
775
|
+
|
|
776
|
+
// User profile
|
|
777
|
+
selectedUserProfile, setSelectedUserProfile,
|
|
778
|
+
pendingMention, setPendingMention,
|
|
779
|
+
|
|
780
|
+
// Notification state
|
|
781
|
+
hasUnreadMessages,
|
|
782
|
+
|
|
783
|
+
// WebSocket event handler
|
|
784
|
+
handlePresenceEvent,
|
|
785
|
+
|
|
786
|
+
// External channel updates
|
|
787
|
+
setChannelsList: channelCtx.setChannelsList,
|
|
788
|
+
appendChannelMessage: sendCtx.appendChannelMessage,
|
|
789
|
+
}), [
|
|
790
|
+
messages, threadMessages, currentChannel, setCurrentChannel,
|
|
791
|
+
currentThread, setCurrentThread, activeThreads, totalUnreadThreadCount,
|
|
792
|
+
sendMessage, isSending, sendError, thread,
|
|
793
|
+
viewMode,
|
|
794
|
+
channelCtx,
|
|
795
|
+
sendCtx,
|
|
796
|
+
currentHuman, selectedDmAgents, removedDmAgents,
|
|
797
|
+
dedupedVisibleMessages, dmParticipantAgents,
|
|
798
|
+
dmSelectedAgentsByHuman, handleDmAgentToggle,
|
|
799
|
+
handleDmSend, handleMainComposerSend,
|
|
800
|
+
onlineUsers, typingUsers, sendTyping, isPresenceConnected,
|
|
801
|
+
humanUsers, humanUnreadCounts,
|
|
802
|
+
markDmSeen,
|
|
803
|
+
selectedUserProfile,
|
|
804
|
+
pendingMention,
|
|
805
|
+
hasUnreadMessages, handlePresenceEvent,
|
|
806
|
+
]);
|
|
807
|
+
|
|
808
|
+
return (
|
|
809
|
+
<MessageContext.Provider value={value}>
|
|
810
|
+
{children}
|
|
811
|
+
</MessageContext.Provider>
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ---------------------------------------------------------------------------
|
|
816
|
+
// Outer Provider (composes ChannelProvider + SendProvider + MessageProviderInner)
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
|
|
819
|
+
export function MessageProvider({ children, data, rawData, enableReactions = false }: MessageProviderProps) {
|
|
820
|
+
return (
|
|
821
|
+
<ChannelProvider>
|
|
822
|
+
<MessageProviderInnerWithSend data={data} rawData={rawData} enableReactions={enableReactions}>
|
|
823
|
+
{children}
|
|
824
|
+
</MessageProviderInnerWithSend>
|
|
825
|
+
</ChannelProvider>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Intermediate wrapper that creates the SendProvider with the local messages
|
|
831
|
+
* that MessageProviderInner will compute. Since SendProvider needs messages
|
|
832
|
+
* from the useMessagesHook (which lives inside MessageProviderInner), we pass
|
|
833
|
+
* them as empty and let SendProvider handle its own message loading.
|
|
834
|
+
*/
|
|
835
|
+
function MessageProviderInnerWithSend({ children, data, rawData, enableReactions }: MessageProviderInnerProps) {
|
|
836
|
+
// We need to pass localMessages to SendProvider for the local channel message fallback.
|
|
837
|
+
// However, the normalized messages are computed inside MessageProviderInner.
|
|
838
|
+
// Since SendProvider only needs them for local (non-cloud) channel message rendering,
|
|
839
|
+
// we derive them here at this level too.
|
|
840
|
+
const { configured: relayConfigured } = useRelayConfigStatus();
|
|
841
|
+
const relayDMsState = useRelayDMs();
|
|
842
|
+
const { currentUser } = useCloudWorkspace();
|
|
843
|
+
|
|
844
|
+
const normalizedRelayMessages = useMemo(() => {
|
|
845
|
+
const sourceMessages = data?.messages ?? [];
|
|
846
|
+
if (!relayConfigured || relayDMsState.conversations.length === 0) {
|
|
847
|
+
return sourceMessages;
|
|
848
|
+
}
|
|
849
|
+
return normalizeRelayDmMessageTargets(sourceMessages, relayDMsState.conversations);
|
|
850
|
+
}, [data?.messages, relayConfigured, relayDMsState.conversations]);
|
|
851
|
+
|
|
852
|
+
const [localUsername] = useState<string | null>(
|
|
853
|
+
typeof window !== 'undefined' ? localStorage.getItem('relay_username') : null
|
|
854
|
+
);
|
|
855
|
+
|
|
856
|
+
return (
|
|
857
|
+
<SendProvider localMessages={normalizedRelayMessages} localUsername={localUsername}>
|
|
858
|
+
<MessageProviderInner data={data} rawData={rawData} enableReactions={enableReactions}>
|
|
859
|
+
{children}
|
|
860
|
+
</MessageProviderInner>
|
|
861
|
+
</SendProvider>
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ---------------------------------------------------------------------------
|
|
866
|
+
// Hook
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
|
|
869
|
+
export function useMessageContext(): MessageContextValue {
|
|
870
|
+
const ctx = useContext(MessageContext);
|
|
871
|
+
if (!ctx) {
|
|
872
|
+
throw new Error('useMessageContext must be used within a MessageProvider');
|
|
873
|
+
}
|
|
874
|
+
return ctx;
|
|
875
|
+
}
|