@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,952 @@
1
+ /**
2
+ * Tests for useDirectMessage hook - DM filtering and deduplication logic
3
+ *
4
+ * TDD approach: Write failing tests first, then fix the implementation.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import type { Agent, Message } from '../../types';
9
+
10
+ // Replicate the filtering and deduplication logic from useDirectMessage hook
11
+ // to test in isolation without React
12
+
13
+ interface DirectMessageTestContext {
14
+ currentHuman: Agent | null;
15
+ currentUserName: string | null;
16
+ messages: Message[];
17
+ agents: Agent[];
18
+ selectedDmAgents: string[];
19
+ removedDmAgents: string[];
20
+ }
21
+
22
+ function computeDmParticipantAgents(ctx: DirectMessageTestContext): string[] {
23
+ const { currentHuman, messages, agents, selectedDmAgents, removedDmAgents } = ctx;
24
+ if (!currentHuman) return [];
25
+
26
+ const agentNameSet = new Set(agents.map((a) => a.name));
27
+ const humanName = currentHuman.name;
28
+ const derived = new Set<string>();
29
+
30
+ for (const msg of messages) {
31
+ const { from, to } = msg;
32
+ if (!from || !to) continue;
33
+ if (from === humanName && agentNameSet.has(to)) derived.add(to);
34
+ if (to === humanName && agentNameSet.has(from)) derived.add(from);
35
+ if (selectedDmAgents.includes(from) && agentNameSet.has(to)) derived.add(to);
36
+ if (selectedDmAgents.includes(to) && agentNameSet.has(from)) derived.add(from);
37
+ }
38
+
39
+ const participants = new Set<string>([...selectedDmAgents, ...derived]);
40
+ removedDmAgents.forEach((a) => participants.delete(a));
41
+ return Array.from(participants);
42
+ }
43
+
44
+ function filterVisibleMessages(ctx: DirectMessageTestContext): Message[] {
45
+ const { currentHuman, currentUserName, messages } = ctx;
46
+ if (!currentHuman) return messages;
47
+
48
+ const dmParticipantAgents = computeDmParticipantAgents(ctx);
49
+ const participants = new Set<string>([currentHuman.name, ...dmParticipantAgents]);
50
+ // Add current user to participants - use "Dashboard" as fallback for local mode
51
+ const effectiveUserName = currentUserName || 'Dashboard';
52
+ participants.add(effectiveUserName);
53
+
54
+ return messages.filter((msg) => {
55
+ if (!msg.from || !msg.to) return false;
56
+ return participants.has(msg.from) && participants.has(msg.to);
57
+ });
58
+ }
59
+
60
+ // Helper to create test messages
61
+ function createMessage(from: string, to: string, content: string, id?: string): Message {
62
+ return {
63
+ id: id || `msg-${Math.random().toString(36).slice(2)}`,
64
+ from,
65
+ to,
66
+ content,
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+ }
70
+
71
+ // Helper to create test agents
72
+ function createAgent(name: string, isHuman = false): Agent {
73
+ return {
74
+ name,
75
+ status: 'online',
76
+ isHuman,
77
+ };
78
+ }
79
+
80
+ describe('useDirectMessage', () => {
81
+ describe('basic DM filtering', () => {
82
+ it('should show messages between current user and human in 1:1 DM', () => {
83
+ const ctx: DirectMessageTestContext = {
84
+ currentHuman: createAgent('alice', true),
85
+ currentUserName: 'bob',
86
+ messages: [
87
+ createMessage('bob', 'alice', 'Hi Alice!'),
88
+ createMessage('alice', 'bob', 'Hi Bob!'),
89
+ ],
90
+ agents: [],
91
+ selectedDmAgents: [],
92
+ removedDmAgents: [],
93
+ };
94
+
95
+ const visible = filterVisibleMessages(ctx);
96
+ expect(visible).toHaveLength(2);
97
+ });
98
+
99
+ it('should filter out messages not involving DM participants', () => {
100
+ const ctx: DirectMessageTestContext = {
101
+ currentHuman: createAgent('alice', true),
102
+ currentUserName: 'bob',
103
+ messages: [
104
+ createMessage('bob', 'alice', 'Hi Alice!'),
105
+ createMessage('charlie', 'dave', 'Unrelated message'),
106
+ ],
107
+ agents: [],
108
+ selectedDmAgents: [],
109
+ removedDmAgents: [],
110
+ };
111
+
112
+ const visible = filterVisibleMessages(ctx);
113
+ expect(visible).toHaveLength(1);
114
+ expect(visible[0].content).toBe('Hi Alice!');
115
+ });
116
+ });
117
+
118
+ describe('group DM with agents', () => {
119
+ it('should show messages when agent is invited to DM', () => {
120
+ const ctx: DirectMessageTestContext = {
121
+ currentHuman: createAgent('alice', true),
122
+ currentUserName: 'bob',
123
+ messages: [
124
+ createMessage('bob', 'alice', 'Hi Alice!'),
125
+ createMessage('bob', 'Agent1', 'Hi Agent1!'),
126
+ ],
127
+ agents: [createAgent('Agent1')],
128
+ selectedDmAgents: ['Agent1'],
129
+ removedDmAgents: [],
130
+ };
131
+
132
+ const visible = filterVisibleMessages(ctx);
133
+ // Both messages should be visible since Agent1 is invited
134
+ expect(visible).toHaveLength(2);
135
+ });
136
+
137
+ /**
138
+ * THE BUG: Agent response should appear in group DM
139
+ *
140
+ * Scenario:
141
+ * - Bob is viewing a DM with Alice
142
+ * - Bob invites Agent1 to the conversation
143
+ * - Bob sends a message (goes to both Alice and Agent1)
144
+ * - Agent1 responds TO BOB
145
+ *
146
+ * Expected: Agent1's response should appear in the group DM view
147
+ * Actual: Agent1's response may be filtered out or appear in wrong place
148
+ */
149
+ it('should show agent response in group DM when agent responds to sender', () => {
150
+ const ctx: DirectMessageTestContext = {
151
+ currentHuman: createAgent('alice', true),
152
+ currentUserName: 'bob',
153
+ messages: [
154
+ createMessage('bob', 'alice', 'Hi Alice!'),
155
+ createMessage('bob', 'Agent1', 'Agent1, help us please'),
156
+ createMessage('Agent1', 'bob', 'Sure, I can help!'), // Agent responds to Bob
157
+ ],
158
+ agents: [createAgent('Agent1')],
159
+ selectedDmAgents: ['Agent1'],
160
+ removedDmAgents: [],
161
+ };
162
+
163
+ const visible = filterVisibleMessages(ctx);
164
+
165
+ // All three messages should be visible:
166
+ // 1. bob -> alice
167
+ // 2. bob -> Agent1
168
+ // 3. Agent1 -> bob (THIS IS THE KEY TEST)
169
+ expect(visible).toHaveLength(3);
170
+ expect(visible.map(m => m.content)).toContain('Sure, I can help!');
171
+ });
172
+
173
+ it('should show agent response when agent responds to human (not current user)', () => {
174
+ const ctx: DirectMessageTestContext = {
175
+ currentHuman: createAgent('alice', true),
176
+ currentUserName: 'bob',
177
+ messages: [
178
+ createMessage('bob', 'alice', 'Hi Alice!'),
179
+ createMessage('bob', 'Agent1', 'Agent1, help Alice'),
180
+ createMessage('Agent1', 'alice', 'Hi Alice, I am here to help'), // Agent responds to Alice
181
+ ],
182
+ agents: [createAgent('Agent1')],
183
+ selectedDmAgents: ['Agent1'],
184
+ removedDmAgents: [],
185
+ };
186
+
187
+ const visible = filterVisibleMessages(ctx);
188
+
189
+ // All three messages should be visible
190
+ expect(visible).toHaveLength(3);
191
+ expect(visible.map(m => m.content)).toContain('Hi Alice, I am here to help');
192
+ });
193
+
194
+ it('should handle multiple agents in group DM', () => {
195
+ const ctx: DirectMessageTestContext = {
196
+ currentHuman: createAgent('alice', true),
197
+ currentUserName: 'bob',
198
+ messages: [
199
+ createMessage('bob', 'alice', 'Group meeting'),
200
+ createMessage('bob', 'Agent1', 'Agent1 invited'),
201
+ createMessage('bob', 'Agent2', 'Agent2 invited'),
202
+ createMessage('Agent1', 'bob', 'Agent1 here'),
203
+ createMessage('Agent2', 'bob', 'Agent2 here'),
204
+ createMessage('Agent1', 'Agent2', 'Agent-to-agent chat'), // Agents talking to each other
205
+ ],
206
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
207
+ selectedDmAgents: ['Agent1', 'Agent2'],
208
+ removedDmAgents: [],
209
+ };
210
+
211
+ const visible = filterVisibleMessages(ctx);
212
+
213
+ // All 6 messages should be visible since all participants are in the group
214
+ expect(visible).toHaveLength(6);
215
+ });
216
+
217
+ it('should NOT show agent messages if agent is removed from DM', () => {
218
+ const ctx: DirectMessageTestContext = {
219
+ currentHuman: createAgent('alice', true),
220
+ currentUserName: 'bob',
221
+ messages: [
222
+ createMessage('bob', 'alice', 'Hi Alice!'),
223
+ createMessage('Agent1', 'bob', 'Message from removed agent'),
224
+ ],
225
+ agents: [createAgent('Agent1')],
226
+ selectedDmAgents: [],
227
+ removedDmAgents: ['Agent1'], // Agent was removed
228
+ };
229
+
230
+ const visible = filterVisibleMessages(ctx);
231
+
232
+ // Only bob->alice should be visible, agent message filtered out
233
+ expect(visible).toHaveLength(1);
234
+ expect(visible[0].content).toBe('Hi Alice!');
235
+ });
236
+ });
237
+
238
+ describe('participant derivation from message history', () => {
239
+ it('should derive agent as participant if human messaged agent', () => {
240
+ const ctx: DirectMessageTestContext = {
241
+ currentHuman: createAgent('alice', true),
242
+ currentUserName: 'bob',
243
+ messages: [
244
+ createMessage('alice', 'Agent1', 'Alice messaged agent directly'),
245
+ ],
246
+ agents: [createAgent('Agent1')],
247
+ selectedDmAgents: [], // Not explicitly selected
248
+ removedDmAgents: [],
249
+ };
250
+
251
+ const participants = computeDmParticipantAgents(ctx);
252
+
253
+ // Agent1 should be derived as participant because alice messaged it
254
+ expect(participants).toContain('Agent1');
255
+ });
256
+
257
+ it('should derive agent as participant if agent messaged human', () => {
258
+ const ctx: DirectMessageTestContext = {
259
+ currentHuman: createAgent('alice', true),
260
+ currentUserName: 'bob',
261
+ messages: [
262
+ createMessage('Agent1', 'alice', 'Agent proactively messaged Alice'),
263
+ ],
264
+ agents: [createAgent('Agent1')],
265
+ selectedDmAgents: [],
266
+ removedDmAgents: [],
267
+ };
268
+
269
+ const participants = computeDmParticipantAgents(ctx);
270
+
271
+ expect(participants).toContain('Agent1');
272
+ });
273
+
274
+ it('should NOT derive agent if it was removed', () => {
275
+ const ctx: DirectMessageTestContext = {
276
+ currentHuman: createAgent('alice', true),
277
+ currentUserName: 'bob',
278
+ messages: [
279
+ createMessage('alice', 'Agent1', 'Before removal'),
280
+ ],
281
+ agents: [createAgent('Agent1')],
282
+ selectedDmAgents: [],
283
+ removedDmAgents: ['Agent1'], // Explicitly removed
284
+ };
285
+
286
+ const participants = computeDmParticipantAgents(ctx);
287
+
288
+ expect(participants).not.toContain('Agent1');
289
+ });
290
+ });
291
+
292
+ describe('edge cases - currentUserName scenarios', () => {
293
+ /**
294
+ * CRITICAL BUG TEST: When currentUserName is null (local mode),
295
+ * the current user is not added to participants, causing agent
296
+ * responses to the current user to be filtered out!
297
+ */
298
+ it('BUG: should show agent response even when currentUserName is null', () => {
299
+ const ctx: DirectMessageTestContext = {
300
+ currentHuman: createAgent('alice', true),
301
+ currentUserName: null, // Local mode - no cloud auth
302
+ messages: [
303
+ // Dashboard sends as "Dashboard" in local mode
304
+ createMessage('Dashboard', 'alice', 'Hi Alice!'),
305
+ createMessage('Dashboard', 'Agent1', 'Agent1, help us'),
306
+ createMessage('Agent1', 'Dashboard', 'Sure, I can help!'), // Agent responds to Dashboard
307
+ ],
308
+ agents: [createAgent('Agent1')],
309
+ selectedDmAgents: ['Agent1'],
310
+ removedDmAgents: [],
311
+ };
312
+
313
+ const visible = filterVisibleMessages(ctx);
314
+
315
+ // BUG: This will likely fail because "Dashboard" is not in participants
316
+ // when currentUserName is null
317
+ expect(visible).toHaveLength(3);
318
+ expect(visible.map(m => m.content)).toContain('Sure, I can help!');
319
+ });
320
+
321
+ it('should handle "Dashboard" as sender when no currentUserName', () => {
322
+ const ctx: DirectMessageTestContext = {
323
+ currentHuman: createAgent('alice', true),
324
+ currentUserName: null,
325
+ messages: [
326
+ createMessage('Dashboard', 'alice', 'Message from local mode'),
327
+ ],
328
+ agents: [],
329
+ selectedDmAgents: [],
330
+ removedDmAgents: [],
331
+ };
332
+
333
+ const visible = filterVisibleMessages(ctx);
334
+
335
+ // In local mode, Dashboard should still be able to send to humans
336
+ // But the filter requires BOTH from and to to be in participants
337
+ // participants = {alice} only when currentUserName is null
338
+ // So "Dashboard" is not in participants!
339
+ expect(visible).toHaveLength(1); // This might fail!
340
+ });
341
+ });
342
+
343
+ describe('edge cases', () => {
344
+ it('should handle empty messages array', () => {
345
+ const ctx: DirectMessageTestContext = {
346
+ currentHuman: createAgent('alice', true),
347
+ currentUserName: 'bob',
348
+ messages: [],
349
+ agents: [createAgent('Agent1')],
350
+ selectedDmAgents: ['Agent1'],
351
+ removedDmAgents: [],
352
+ };
353
+
354
+ const visible = filterVisibleMessages(ctx);
355
+ expect(visible).toHaveLength(0);
356
+ });
357
+
358
+ it('should handle null currentHuman', () => {
359
+ const ctx: DirectMessageTestContext = {
360
+ currentHuman: null,
361
+ currentUserName: 'bob',
362
+ messages: [createMessage('bob', 'alice', 'Test')],
363
+ agents: [],
364
+ selectedDmAgents: [],
365
+ removedDmAgents: [],
366
+ };
367
+
368
+ const visible = filterVisibleMessages(ctx);
369
+ // Should return all messages when not in DM mode
370
+ expect(visible).toHaveLength(1);
371
+ });
372
+
373
+ it('should handle messages with missing from/to', () => {
374
+ const ctx: DirectMessageTestContext = {
375
+ currentHuman: createAgent('alice', true),
376
+ currentUserName: 'bob',
377
+ messages: [
378
+ { id: '1', content: 'No from', to: 'alice', timestamp: new Date().toISOString() } as Message,
379
+ { id: '2', content: 'No to', from: 'bob', timestamp: new Date().toISOString() } as Message,
380
+ createMessage('bob', 'alice', 'Valid message'),
381
+ ],
382
+ agents: [],
383
+ selectedDmAgents: [],
384
+ removedDmAgents: [],
385
+ };
386
+
387
+ const visible = filterVisibleMessages(ctx);
388
+ // Only valid message should be included
389
+ expect(visible).toHaveLength(1);
390
+ expect(visible[0].content).toBe('Valid message');
391
+ });
392
+
393
+ it('should be case-sensitive for participant matching', () => {
394
+ const ctx: DirectMessageTestContext = {
395
+ currentHuman: createAgent('Alice', true), // Capital A
396
+ currentUserName: 'Bob', // Capital B
397
+ messages: [
398
+ createMessage('Bob', 'Alice', 'Correct case'),
399
+ createMessage('bob', 'alice', 'Wrong case'), // lowercase - should NOT match
400
+ ],
401
+ agents: [],
402
+ selectedDmAgents: [],
403
+ removedDmAgents: [],
404
+ };
405
+
406
+ const visible = filterVisibleMessages(ctx);
407
+ // Only correctly-cased message should appear
408
+ // (Note: This may need to be case-insensitive depending on requirements)
409
+ expect(visible).toHaveLength(1);
410
+ expect(visible[0].content).toBe('Correct case');
411
+ });
412
+ });
413
+
414
+ describe('advanced agent derivation', () => {
415
+ it('should derive second agent when selected agent messages another agent', () => {
416
+ const ctx: DirectMessageTestContext = {
417
+ currentHuman: createAgent('alice', true),
418
+ currentUserName: 'bob',
419
+ messages: [
420
+ createMessage('Agent1', 'Agent2', 'Agent1 talking to Agent2'),
421
+ ],
422
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
423
+ selectedDmAgents: ['Agent1'], // Only Agent1 explicitly selected
424
+ removedDmAgents: [],
425
+ };
426
+
427
+ const participants = computeDmParticipantAgents(ctx);
428
+
429
+ // Agent2 should be derived because Agent1 (selected) messaged it
430
+ expect(participants).toContain('Agent1');
431
+ expect(participants).toContain('Agent2');
432
+ });
433
+
434
+ it('should derive agent chain: agent1 → agent2 → agent3', () => {
435
+ const ctx: DirectMessageTestContext = {
436
+ currentHuman: createAgent('alice', true),
437
+ currentUserName: 'bob',
438
+ messages: [
439
+ createMessage('alice', 'Agent1', 'Alice starts with Agent1'),
440
+ createMessage('Agent1', 'Agent2', 'Agent1 brings in Agent2'),
441
+ // Note: Agent2 -> Agent3 won't derive Agent3 because Agent2 wasn't in selectedDmAgents
442
+ // This tests the current behavior - derivation only goes one level deep from selected agents
443
+ ],
444
+ agents: [createAgent('Agent1'), createAgent('Agent2'), createAgent('Agent3')],
445
+ selectedDmAgents: [], // No explicit selection, relying on derivation
446
+ removedDmAgents: [],
447
+ };
448
+
449
+ const participants = computeDmParticipantAgents(ctx);
450
+
451
+ // Agent1 derived from alice -> Agent1
452
+ expect(participants).toContain('Agent1');
453
+ // Agent2 NOT derived because Agent1 wasn't in selectedDmAgents
454
+ // (derivation requires explicit selection or human involvement)
455
+ expect(participants).not.toContain('Agent2');
456
+ });
457
+
458
+ it('should derive agents from both human and selected agent interactions', () => {
459
+ const ctx: DirectMessageTestContext = {
460
+ currentHuman: createAgent('alice', true),
461
+ currentUserName: 'bob',
462
+ messages: [
463
+ createMessage('alice', 'Agent1', 'Alice to Agent1'),
464
+ createMessage('Agent1', 'Agent2', 'Agent1 to Agent2'),
465
+ ],
466
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
467
+ selectedDmAgents: ['Agent1'], // Agent1 explicitly selected
468
+ removedDmAgents: [],
469
+ };
470
+
471
+ const participants = computeDmParticipantAgents(ctx);
472
+
473
+ // Both should be derived
474
+ expect(participants).toContain('Agent1'); // From alice -> Agent1 AND selectedDmAgents
475
+ expect(participants).toContain('Agent2'); // From Agent1 (selected) -> Agent2
476
+ });
477
+ });
478
+
479
+ describe('complex removal scenarios', () => {
480
+ it('should handle removing one agent from multiple agents', () => {
481
+ const ctx: DirectMessageTestContext = {
482
+ currentHuman: createAgent('alice', true),
483
+ currentUserName: 'bob',
484
+ messages: [
485
+ createMessage('bob', 'Agent1', 'To Agent1'),
486
+ createMessage('bob', 'Agent2', 'To Agent2'),
487
+ createMessage('bob', 'Agent3', 'To Agent3'),
488
+ ],
489
+ agents: [createAgent('Agent1'), createAgent('Agent2'), createAgent('Agent3')],
490
+ selectedDmAgents: ['Agent1', 'Agent2', 'Agent3'],
491
+ removedDmAgents: ['Agent2'], // Only Agent2 removed
492
+ };
493
+
494
+ const participants = computeDmParticipantAgents(ctx);
495
+
496
+ expect(participants).toContain('Agent1');
497
+ expect(participants).not.toContain('Agent2');
498
+ expect(participants).toContain('Agent3');
499
+ });
500
+
501
+ it('should filter messages involving removed agent', () => {
502
+ const ctx: DirectMessageTestContext = {
503
+ currentHuman: createAgent('alice', true),
504
+ currentUserName: 'bob',
505
+ messages: [
506
+ createMessage('bob', 'alice', 'To Alice'),
507
+ createMessage('bob', 'Agent1', 'To Agent1'),
508
+ createMessage('Agent1', 'bob', 'From Agent1'),
509
+ createMessage('bob', 'Agent2', 'To removed Agent2'),
510
+ createMessage('Agent2', 'bob', 'From removed Agent2'),
511
+ ],
512
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
513
+ selectedDmAgents: ['Agent1', 'Agent2'],
514
+ removedDmAgents: ['Agent2'],
515
+ };
516
+
517
+ const visible = filterVisibleMessages(ctx);
518
+
519
+ // Should see: bob->alice, bob->Agent1, Agent1->bob
520
+ // Should NOT see: bob->Agent2, Agent2->bob
521
+ expect(visible).toHaveLength(3);
522
+ expect(visible.map(m => m.content)).not.toContain('To removed Agent2');
523
+ expect(visible.map(m => m.content)).not.toContain('From removed Agent2');
524
+ });
525
+
526
+ it('should handle re-adding agent after removal via selection', () => {
527
+ // Scenario: Agent was removed but is now back in selectedDmAgents
528
+ const ctx: DirectMessageTestContext = {
529
+ currentHuman: createAgent('alice', true),
530
+ currentUserName: 'bob',
531
+ messages: [
532
+ createMessage('Agent1', 'bob', 'Agent1 message'),
533
+ ],
534
+ agents: [createAgent('Agent1')],
535
+ selectedDmAgents: ['Agent1'], // Re-added via selection
536
+ removedDmAgents: [], // No longer in removed list
537
+ };
538
+
539
+ const participants = computeDmParticipantAgents(ctx);
540
+ const visible = filterVisibleMessages(ctx);
541
+
542
+ expect(participants).toContain('Agent1');
543
+ expect(visible).toHaveLength(1);
544
+ });
545
+ });
546
+
547
+ describe('agent-to-agent communication', () => {
548
+ it('should show agent-to-agent messages when both are participants', () => {
549
+ const ctx: DirectMessageTestContext = {
550
+ currentHuman: createAgent('alice', true),
551
+ currentUserName: 'bob',
552
+ messages: [
553
+ createMessage('Agent1', 'Agent2', 'Collaboration message'),
554
+ createMessage('Agent2', 'Agent1', 'Response'),
555
+ ],
556
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
557
+ selectedDmAgents: ['Agent1', 'Agent2'],
558
+ removedDmAgents: [],
559
+ };
560
+
561
+ const visible = filterVisibleMessages(ctx);
562
+
563
+ expect(visible).toHaveLength(2);
564
+ });
565
+
566
+ it('should derive Agent2 when Agent1 (selected) messages it', () => {
567
+ // This tests that derivation works: when a selected agent messages another agent,
568
+ // the recipient agent becomes a derived participant
569
+ const ctx: DirectMessageTestContext = {
570
+ currentHuman: createAgent('alice', true),
571
+ currentUserName: 'bob',
572
+ messages: [
573
+ createMessage('Agent1', 'Agent2', 'Message triggers derivation'),
574
+ ],
575
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
576
+ selectedDmAgents: ['Agent1'], // Only Agent1 is selected
577
+ removedDmAgents: [],
578
+ };
579
+
580
+ const participants = computeDmParticipantAgents(ctx);
581
+ const visible = filterVisibleMessages(ctx);
582
+
583
+ // Agent2 gets derived because Agent1 (selected) messaged it
584
+ expect(participants).toContain('Agent2');
585
+ expect(visible).toHaveLength(1);
586
+ });
587
+
588
+ it('should NOT show message from non-participant to non-participant', () => {
589
+ const ctx: DirectMessageTestContext = {
590
+ currentHuman: createAgent('alice', true),
591
+ currentUserName: 'bob',
592
+ messages: [
593
+ createMessage('Agent3', 'Agent4', 'Neither agent is a participant'),
594
+ ],
595
+ agents: [createAgent('Agent1'), createAgent('Agent3'), createAgent('Agent4')],
596
+ selectedDmAgents: ['Agent1'], // Only Agent1 is selected, not Agent3 or Agent4
597
+ removedDmAgents: [],
598
+ };
599
+
600
+ const visible = filterVisibleMessages(ctx);
601
+
602
+ // Neither Agent3 nor Agent4 are participants, so message is filtered
603
+ expect(visible).toHaveLength(0);
604
+ });
605
+ });
606
+
607
+ describe('human participant scenarios', () => {
608
+ it('should show human (currentHuman) initiating conversation with agent', () => {
609
+ const ctx: DirectMessageTestContext = {
610
+ currentHuman: createAgent('alice', true),
611
+ currentUserName: 'bob',
612
+ messages: [
613
+ createMessage('alice', 'Agent1', 'Alice starts the conversation'),
614
+ createMessage('Agent1', 'alice', 'Agent1 responds to Alice'),
615
+ ],
616
+ agents: [createAgent('Agent1')],
617
+ selectedDmAgents: ['Agent1'],
618
+ removedDmAgents: [],
619
+ };
620
+
621
+ const visible = filterVisibleMessages(ctx);
622
+
623
+ expect(visible).toHaveLength(2);
624
+ });
625
+
626
+ it('should show messages from human to current user', () => {
627
+ const ctx: DirectMessageTestContext = {
628
+ currentHuman: createAgent('alice', true),
629
+ currentUserName: 'bob',
630
+ messages: [
631
+ createMessage('alice', 'bob', 'Alice messages Bob directly'),
632
+ createMessage('bob', 'alice', 'Bob responds'),
633
+ ],
634
+ agents: [],
635
+ selectedDmAgents: [],
636
+ removedDmAgents: [],
637
+ };
638
+
639
+ const visible = filterVisibleMessages(ctx);
640
+
641
+ expect(visible).toHaveLength(2);
642
+ });
643
+ });
644
+
645
+ describe('cloud mode scenarios', () => {
646
+ it('should work with GitHub username as currentUserName', () => {
647
+ const ctx: DirectMessageTestContext = {
648
+ currentHuman: createAgent('alice', true),
649
+ currentUserName: 'github-user-1', // GitHub username (generic)
650
+ messages: [
651
+ createMessage('github-user-1', 'alice', 'Hi from GitHub user'),
652
+ createMessage('alice', 'github-user-1', 'Hi back!'),
653
+ createMessage('github-user-1', 'Agent1', 'Agent help'),
654
+ createMessage('Agent1', 'github-user-1', 'Agent response'),
655
+ ],
656
+ agents: [createAgent('Agent1')],
657
+ selectedDmAgents: ['Agent1'],
658
+ removedDmAgents: [],
659
+ };
660
+
661
+ const visible = filterVisibleMessages(ctx);
662
+
663
+ expect(visible).toHaveLength(4);
664
+ });
665
+
666
+ it('should handle mixed Dashboard and username messages', () => {
667
+ // Edge case: what if both Dashboard and username appear in messages?
668
+ const ctx: DirectMessageTestContext = {
669
+ currentHuman: createAgent('alice', true),
670
+ currentUserName: 'bob', // Cloud mode with username
671
+ messages: [
672
+ createMessage('Dashboard', 'alice', 'Old local mode message'),
673
+ createMessage('bob', 'alice', 'New cloud mode message'),
674
+ ],
675
+ agents: [],
676
+ selectedDmAgents: [],
677
+ removedDmAgents: [],
678
+ };
679
+
680
+ const visible = filterVisibleMessages(ctx);
681
+
682
+ // Only bob->alice should show (Dashboard is not bob)
683
+ // This tests that we correctly use currentUserName when provided
684
+ expect(visible).toHaveLength(1);
685
+ expect(visible[0].content).toBe('New cloud mode message');
686
+ });
687
+
688
+ // CLOUD MODE SPECIFIC TESTS - currentUserName is NEVER null in cloud
689
+ it('CLOUD: should route agent responses correctly with GitHub username', () => {
690
+ // This is the original bug scenario but in cloud mode
691
+ const ctx: DirectMessageTestContext = {
692
+ currentHuman: createAgent('alice', true),
693
+ currentUserName: 'github-user-1', // Cloud mode - always has username
694
+ messages: [
695
+ createMessage('github-user-1', 'alice', 'Hi Alice!'),
696
+ createMessage('github-user-1', 'Agent1', 'Agent1, help us please'),
697
+ createMessage('Agent1', 'github-user-1', 'Sure, I can help!'), // Agent responds to user
698
+ ],
699
+ agents: [createAgent('Agent1')],
700
+ selectedDmAgents: ['Agent1'],
701
+ removedDmAgents: [],
702
+ };
703
+
704
+ const visible = filterVisibleMessages(ctx);
705
+
706
+ // All messages should be visible - agent response stays in group DM
707
+ expect(visible).toHaveLength(3);
708
+ expect(visible.map(m => m.content)).toContain('Sure, I can help!');
709
+ });
710
+
711
+ it('CLOUD: should handle multiple agents with GitHub username', () => {
712
+ const ctx: DirectMessageTestContext = {
713
+ currentHuman: createAgent('alice', true),
714
+ currentUserName: 'github-user-2', // Different GitHub user
715
+ messages: [
716
+ createMessage('github-user-2', 'alice', 'Team meeting'),
717
+ createMessage('github-user-2', 'Frontend', 'Frontend agent invited'),
718
+ createMessage('github-user-2', 'Backend', 'Backend agent invited'),
719
+ createMessage('Frontend', 'github-user-2', 'Frontend here'),
720
+ createMessage('Backend', 'github-user-2', 'Backend here'),
721
+ createMessage('Frontend', 'Backend', 'Agent collaboration'),
722
+ ],
723
+ agents: [createAgent('Frontend'), createAgent('Backend')],
724
+ selectedDmAgents: ['Frontend', 'Backend'],
725
+ removedDmAgents: [],
726
+ };
727
+
728
+ const visible = filterVisibleMessages(ctx);
729
+
730
+ // All 6 messages visible
731
+ expect(visible).toHaveLength(6);
732
+ });
733
+
734
+ it('CLOUD: currentUserName is never null - Dashboard fallback not used', () => {
735
+ // Verify that when username is provided, "Dashboard" is NOT in participants
736
+ const ctx: DirectMessageTestContext = {
737
+ currentHuman: createAgent('alice', true),
738
+ currentUserName: 'github-user-1', // Cloud mode
739
+ messages: [
740
+ createMessage('Dashboard', 'alice', 'This should NOT appear'),
741
+ createMessage('github-user-1', 'alice', 'This should appear'),
742
+ ],
743
+ agents: [],
744
+ selectedDmAgents: [],
745
+ removedDmAgents: [],
746
+ };
747
+
748
+ const visible = filterVisibleMessages(ctx);
749
+
750
+ // Dashboard message filtered out - only github-user-1 messages shown
751
+ expect(visible).toHaveLength(1);
752
+ expect(visible[0].from).toBe('github-user-1');
753
+ });
754
+
755
+ it('CLOUD: agent removal works with GitHub username', () => {
756
+ const ctx: DirectMessageTestContext = {
757
+ currentHuman: createAgent('alice', true),
758
+ currentUserName: 'github-user-1',
759
+ messages: [
760
+ createMessage('github-user-1', 'Agent1', 'To Agent1'),
761
+ createMessage('Agent1', 'github-user-1', 'From Agent1'),
762
+ createMessage('github-user-1', 'Agent2', 'To Agent2'),
763
+ createMessage('Agent2', 'github-user-1', 'From Agent2'),
764
+ ],
765
+ agents: [createAgent('Agent1'), createAgent('Agent2')],
766
+ selectedDmAgents: ['Agent1', 'Agent2'],
767
+ removedDmAgents: ['Agent2'], // Agent2 removed
768
+ };
769
+
770
+ const visible = filterVisibleMessages(ctx);
771
+
772
+ // Only Agent1 messages should be visible
773
+ expect(visible).toHaveLength(2);
774
+ expect(visible.every(m => m.from === 'Agent1' || m.to === 'Agent1')).toBe(true);
775
+ });
776
+ });
777
+
778
+ describe('boundary conditions', () => {
779
+ it('should handle many agents (5+)', () => {
780
+ const agents = ['Agent1', 'Agent2', 'Agent3', 'Agent4', 'Agent5', 'Agent6'].map(
781
+ name => createAgent(name)
782
+ );
783
+ const ctx: DirectMessageTestContext = {
784
+ currentHuman: createAgent('alice', true),
785
+ currentUserName: 'bob',
786
+ messages: agents.map(a => createMessage('bob', a.name, `Message to ${a.name}`)),
787
+ agents,
788
+ selectedDmAgents: agents.map(a => a.name),
789
+ removedDmAgents: [],
790
+ };
791
+
792
+ const visible = filterVisibleMessages(ctx);
793
+
794
+ expect(visible).toHaveLength(6);
795
+ });
796
+
797
+ it('should handle conversation with many message exchanges', () => {
798
+ const messages: Message[] = [];
799
+ for (let i = 0; i < 50; i++) {
800
+ messages.push(createMessage('bob', 'alice', `Message ${i}`));
801
+ messages.push(createMessage('alice', 'bob', `Response ${i}`));
802
+ messages.push(createMessage('bob', 'Agent1', `Agent message ${i}`));
803
+ messages.push(createMessage('Agent1', 'bob', `Agent response ${i}`));
804
+ }
805
+
806
+ const ctx: DirectMessageTestContext = {
807
+ currentHuman: createAgent('alice', true),
808
+ currentUserName: 'bob',
809
+ messages,
810
+ agents: [createAgent('Agent1')],
811
+ selectedDmAgents: ['Agent1'],
812
+ removedDmAgents: [],
813
+ };
814
+
815
+ const visible = filterVisibleMessages(ctx);
816
+
817
+ expect(visible).toHaveLength(200); // 50 * 4 messages
818
+ });
819
+
820
+ it('should handle agent with same prefix as human name', () => {
821
+ const ctx: DirectMessageTestContext = {
822
+ currentHuman: createAgent('alice', true),
823
+ currentUserName: 'bob',
824
+ messages: [
825
+ createMessage('bob', 'alice', 'To human alice'),
826
+ createMessage('bob', 'alice-agent', 'To agent alice-agent'),
827
+ ],
828
+ agents: [createAgent('alice-agent')], // Agent name starts with human name
829
+ selectedDmAgents: ['alice-agent'],
830
+ removedDmAgents: [],
831
+ };
832
+
833
+ const visible = filterVisibleMessages(ctx);
834
+
835
+ expect(visible).toHaveLength(2);
836
+ });
837
+ });
838
+
839
+ describe('GitHub username vs Dashboard routing', () => {
840
+ /**
841
+ * BUG FIX TEST: Messages to GitHub username should be visible
842
+ *
843
+ * When an agent sends a message TO a user's GitHub username,
844
+ * the message should appear in the user's DM view.
845
+ *
846
+ * Scenario:
847
+ * - User "khaliqgant" is viewing DMs
848
+ * - Lead agent sends message to "khaliqgant"
849
+ * - Message should be visible to khaliqgant
850
+ */
851
+ it('should show messages sent TO user GitHub username', () => {
852
+ const ctx: DirectMessageTestContext = {
853
+ currentHuman: createAgent('khaliqgant', true), // Viewing own DMs
854
+ currentUserName: 'khaliqgant', // User's GitHub username
855
+ messages: [
856
+ createMessage('Lead', 'khaliqgant', 'Hello khaliqgant, here is an update'),
857
+ ],
858
+ agents: [createAgent('Lead')], // Lead is an AI agent
859
+ selectedDmAgents: ['Lead'], // Lead was invited to conversation
860
+ removedDmAgents: [],
861
+ };
862
+
863
+ const visible = filterVisibleMessages(ctx);
864
+
865
+ // Message from Lead to khaliqgant should be visible
866
+ expect(visible).toHaveLength(1);
867
+ expect(visible[0].content).toBe('Hello khaliqgant, here is an update');
868
+ });
869
+
870
+ /**
871
+ * Dashboard messages should NOT appear in DM view
872
+ *
873
+ * Dashboard is a system client, not a real participant.
874
+ * Messages addressed to Dashboard should not be shown.
875
+ */
876
+ it('should NOT show messages sent TO Dashboard', () => {
877
+ const ctx: DirectMessageTestContext = {
878
+ currentHuman: createAgent('khaliqgant', true),
879
+ currentUserName: 'khaliqgant',
880
+ messages: [
881
+ createMessage('Lead', 'Dashboard', 'Message to system client'),
882
+ ],
883
+ agents: [createAgent('Lead')],
884
+ selectedDmAgents: ['Lead'],
885
+ removedDmAgents: [],
886
+ };
887
+
888
+ const visible = filterVisibleMessages(ctx);
889
+
890
+ // Message to Dashboard should NOT be visible (it's not a participant)
891
+ expect(visible).toHaveLength(0);
892
+ });
893
+
894
+ /**
895
+ * Both GitHub username AND Dashboard messages in same conversation
896
+ *
897
+ * When an agent sends to both targets, only the GitHub username
898
+ * message should be visible.
899
+ */
900
+ it('should show GitHub username messages but not Dashboard messages', () => {
901
+ const ctx: DirectMessageTestContext = {
902
+ currentHuman: createAgent('khaliqgant', true),
903
+ currentUserName: 'khaliqgant',
904
+ messages: [
905
+ createMessage('Lead', 'khaliqgant', 'Direct to user'),
906
+ createMessage('Lead', 'Dashboard', 'To system client'),
907
+ createMessage('khaliqgant', 'Lead', 'Reply from user'),
908
+ ],
909
+ agents: [createAgent('Lead')],
910
+ selectedDmAgents: ['Lead'],
911
+ removedDmAgents: [],
912
+ };
913
+
914
+ const visible = filterVisibleMessages(ctx);
915
+
916
+ // Only 2 messages visible (Lead->khaliqgant and khaliqgant->Lead)
917
+ // Lead->Dashboard should be filtered out
918
+ expect(visible).toHaveLength(2);
919
+ expect(visible.map(m => m.to)).not.toContain('Dashboard');
920
+ });
921
+
922
+ /**
923
+ * Edge case: currentUserName is different from currentHuman.name
924
+ *
925
+ * When viewing another user's DMs (e.g., admin view),
926
+ * messages to currentUserName should still be filtered correctly.
927
+ */
928
+ it('should use effectiveUserName (currentUserName) for filtering, not currentHuman.name', () => {
929
+ const ctx: DirectMessageTestContext = {
930
+ currentHuman: createAgent('alice', true), // Viewing DMs with alice
931
+ currentUserName: 'bob', // Current logged-in user is bob
932
+ messages: [
933
+ createMessage('Lead', 'bob', 'Message to bob (current user)'),
934
+ createMessage('Lead', 'alice', 'Message to alice (DM partner)'),
935
+ createMessage('Lead', 'charlie', 'Message to charlie (third party)'),
936
+ ],
937
+ agents: [createAgent('Lead')],
938
+ selectedDmAgents: ['Lead'],
939
+ removedDmAgents: [],
940
+ };
941
+
942
+ const visible = filterVisibleMessages(ctx);
943
+
944
+ // Lead->bob and Lead->alice should be visible
945
+ // Lead->charlie should NOT be visible
946
+ expect(visible).toHaveLength(2);
947
+ expect(visible.map(m => m.to)).toContain('bob');
948
+ expect(visible.map(m => m.to)).toContain('alice');
949
+ expect(visible.map(m => m.to)).not.toContain('charlie');
950
+ });
951
+ });
952
+ });