@agent-relay/dashboard 2.0.86 → 2.0.88

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 (73) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/{1028-53c5a7e2453505f8.js → 1028-ff682899d23dc669.js} +1 -1
  3. package/out/_next/static/chunks/3238-24c1e4b1cefe3c71.js +73 -0
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-3dffd65b6344f53e.js → page-f33c6214e21ccfdd.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-09ce10603ad9a251.js → page-2e38080856c3c293.js} +1 -1
  6. package/out/about.html +1 -1
  7. package/out/about.txt +1 -1
  8. package/out/app/onboarding.html +1 -1
  9. package/out/app/onboarding.txt +1 -1
  10. package/out/app.html +1 -1
  11. package/out/app.txt +2 -2
  12. package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +1 -1
  13. package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
  14. package/out/blog/let-them-cook-multi-agent-orchestration.html +1 -1
  15. package/out/blog/let-them-cook-multi-agent-orchestration.txt +1 -1
  16. package/out/blog.html +1 -1
  17. package/out/blog.txt +1 -1
  18. package/out/careers.html +1 -1
  19. package/out/careers.txt +1 -1
  20. package/out/changelog.html +1 -1
  21. package/out/changelog.txt +1 -1
  22. package/out/cloud/link.html +1 -1
  23. package/out/cloud/link.txt +1 -1
  24. package/out/complete-profile.html +1 -1
  25. package/out/complete-profile.txt +1 -1
  26. package/out/connect-repos.html +1 -1
  27. package/out/connect-repos.txt +1 -1
  28. package/out/contact.html +1 -1
  29. package/out/contact.txt +1 -1
  30. package/out/dev/cli-tools.html +1 -1
  31. package/out/dev/cli-tools.txt +1 -1
  32. package/out/dev/log-viewer.html +1 -1
  33. package/out/dev/log-viewer.txt +1 -1
  34. package/out/docs.html +1 -1
  35. package/out/docs.txt +1 -1
  36. package/out/history.html +1 -1
  37. package/out/history.txt +1 -1
  38. package/out/index.html +1 -1
  39. package/out/index.txt +2 -2
  40. package/out/login.html +1 -1
  41. package/out/login.txt +1 -1
  42. package/out/metrics.html +1 -1
  43. package/out/metrics.txt +1 -1
  44. package/out/pricing.html +1 -1
  45. package/out/pricing.txt +1 -1
  46. package/out/privacy.html +1 -1
  47. package/out/privacy.txt +1 -1
  48. package/out/providers/setup/claude.html +1 -1
  49. package/out/providers/setup/claude.txt +1 -1
  50. package/out/providers/setup/codex.html +1 -1
  51. package/out/providers/setup/codex.txt +1 -1
  52. package/out/providers/setup/cursor.html +1 -1
  53. package/out/providers/setup/cursor.txt +1 -1
  54. package/out/providers.html +1 -1
  55. package/out/providers.txt +1 -1
  56. package/out/security.html +1 -1
  57. package/out/security.txt +1 -1
  58. package/out/signup.html +1 -1
  59. package/out/signup.txt +1 -1
  60. package/out/terms.html +1 -1
  61. package/out/terms.txt +1 -1
  62. package/package.json +4 -3
  63. package/src/components/ChannelChat.tsx +1 -1
  64. package/src/components/ChannelSidebar.tsx +2 -2
  65. package/src/lib/relaycastMessageAdapters.test.ts +30 -1
  66. package/src/lib/relaycastMessageAdapters.ts +28 -7
  67. package/src/providers/ChannelProvider.tsx +15 -4
  68. package/src/providers/MessageProvider.tsx +4 -3
  69. package/src/providers/RelayConfigProvider.tsx +15 -0
  70. package/src/providers/SendProvider.tsx +38 -4
  71. package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +0 -73
  72. /package/out/_next/static/{2BWmI-66Pm2lpI_cbat7z → hDG4FHMjeVX6ES941qVjL}/_buildManifest.js +0 -0
  73. /package/out/_next/static/{2BWmI-66Pm2lpI_cbat7z → hDG4FHMjeVX6ES941qVjL}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
2
  import type { Message } from '../types';
3
- import { normalizeRelayDmMessageTargets } from './relaycastMessageAdapters.js';
3
+ import { getRelayDmParticipantName, normalizeRelayDmMessageTargets } from './relaycastMessageAdapters.js';
4
4
 
5
5
  function setRelayUsername(value?: string): void {
6
6
  const storage = (globalThis as { localStorage?: Storage }).localStorage;
@@ -93,6 +93,35 @@ describe('normalizeRelayDmMessageTargets', () => {
93
93
  expect(normalized[0]?.to).toBe('Natty');
94
94
  });
95
95
 
96
+ it('maps dm_* targets to object participants using agent_name', () => {
97
+ const messages: Message[] = [
98
+ {
99
+ id: 'msg-2a',
100
+ from: 'Natty',
101
+ to: 'dm_7b62c72644b9316e7e10a992',
102
+ content: 'hello',
103
+ timestamp: '2026-02-24T12:00:06.000Z',
104
+ },
105
+ ];
106
+
107
+ const normalized = normalizeRelayDmMessageTargets(messages, [
108
+ {
109
+ id: 'dm_7b62c72644b9316e7e10a992',
110
+ participants: [{ agent_name: 'Natty' }, { agent_name: 'test-broker-new' }],
111
+ },
112
+ ]);
113
+
114
+ expect(normalized[0]?.to).toBe('test-broker-new');
115
+ });
116
+
117
+ it('normalizes participant names using getRelayDmParticipantName', () => {
118
+ expect(getRelayDmParticipantName({ agent_name: 'Lead', name: 'ignored' })).toBe('Lead');
119
+ expect(getRelayDmParticipantName({ agentName: 'Codex-Worker' })).toBe('Codex-Worker');
120
+ expect(getRelayDmParticipantName('Test-Broker')).toBe('Test-Broker');
121
+ expect(getRelayDmParticipantName({ username: 'human-user' })).toBe('human-user');
122
+ expect(getRelayDmParticipantName(123)).toBeNull();
123
+ });
124
+
96
125
  it('leaves non-dm and unknown dm targets unchanged', () => {
97
126
  const messages: Message[] = [
98
127
  {
@@ -12,6 +12,30 @@ type RelayDmConversationLike = {
12
12
  participants: unknown[];
13
13
  };
14
14
 
15
+ export function getRelayDmParticipantName(participant: unknown): string | null {
16
+ if (typeof participant === 'string') {
17
+ return normalizeRelayIdentity(participant);
18
+ }
19
+
20
+ if (participant === null || typeof participant !== 'object') {
21
+ return null;
22
+ }
23
+
24
+ const record = participant as Record<string, unknown>;
25
+ const rawName = (
26
+ record.agent_name
27
+ ?? record.agentName
28
+ ?? record.name
29
+ ?? record.username
30
+ );
31
+
32
+ if (typeof rawName !== 'string') {
33
+ return null;
34
+ }
35
+
36
+ return normalizeRelayIdentity(rawName);
37
+ }
38
+
15
39
  function normalizeRelayIdentity(value: string): string {
16
40
  const trimmed = value.trim();
17
41
  if (!trimmed) return '';
@@ -39,8 +63,7 @@ function resolveDmRecipient(
39
63
  const senderKey = normalizeRelayIdentity(sender).toLowerCase();
40
64
 
41
65
  for (const participant of participants) {
42
- if (typeof participant !== 'string') continue;
43
- const normalized = normalizeRelayIdentity(participant);
66
+ const normalized = getRelayDmParticipantName(participant);
44
67
  if (!normalized) continue;
45
68
  if (normalized.toLowerCase() !== senderKey) {
46
69
  return normalized;
@@ -48,11 +71,9 @@ function resolveDmRecipient(
48
71
  }
49
72
 
50
73
  for (const participant of participants) {
51
- if (typeof participant !== 'string') continue;
52
- const normalized = normalizeRelayIdentity(participant);
53
- if (normalized) {
54
- return normalized;
55
- }
74
+ const normalized = getRelayDmParticipantName(participant);
75
+ if (!normalized) continue;
76
+ return normalized;
56
77
  }
57
78
 
58
79
  return null;
@@ -118,9 +118,20 @@ export function ChannelProvider({ children }: ChannelProviderProps) {
118
118
 
119
119
  // Relay channel state
120
120
  const relayChannelsState = useRelayChannels();
121
+ const relayChannelsLoading = relayChannelsState.loading;
122
+ const relayChannelsRaw = relayChannelsState.channels;
123
+
124
+ // Stabilize the mapped channels array — only recompute when the serialized
125
+ // channel list actually changes (avoids infinite re-render loops from new
126
+ // array references returned by the relay hook on every render).
127
+ const relayChannelsKey = useMemo(
128
+ () => JSON.stringify(relayChannelsRaw.map(c => c.name + ':' + (c.topic ?? '') + ':' + (c.isArchived ?? false) + ':' + (c.memberCount ?? 0))),
129
+ [relayChannelsRaw],
130
+ );
121
131
  const relayMappedChannels = useMemo(
122
- () => relayChannelsState.channels.map(mapRelayChannelToDashboard),
123
- [relayChannelsState.channels],
132
+ () => relayChannelsRaw.map(mapRelayChannelToDashboard),
133
+ // eslint-disable-next-line react-hooks/exhaustive-deps
134
+ [relayChannelsKey],
124
135
  );
125
136
 
126
137
  // Channel list state
@@ -225,7 +236,7 @@ export function ChannelProvider({ children }: ChannelProviderProps) {
225
236
  const activeChannels = relayMappedChannels.filter((channel) => channel.status !== 'archived');
226
237
  const archivedChannels = relayMappedChannels.filter((channel) => channel.status === 'archived');
227
238
  setChannelListsFromResponse({ channels: activeChannels, archivedChannels });
228
- setIsChannelsLoading(relayChannelsState.loading);
239
+ setIsChannelsLoading(relayChannelsLoading);
229
240
  return;
230
241
  }
231
242
 
@@ -253,7 +264,7 @@ export function ChannelProvider({ children }: ChannelProviderProps) {
253
264
  fetchChannels();
254
265
  }, [
255
266
  relayConfigured,
256
- relayChannelsState,
267
+ relayChannelsLoading,
257
268
  relayMappedChannels,
258
269
  effectiveActiveWorkspaceId,
259
270
  isWorkspaceFeaturesEnabled,
@@ -25,6 +25,7 @@ import { useRelayConfigStatus } from './RelayConfigProvider';
25
25
  import { isDashboardVariant } from '../lib/identity';
26
26
  import {
27
27
  normalizeRelayDmMessageTargets,
28
+ getRelayDmParticipantName,
28
29
  } from '../lib/relaycastMessageAdapters';
29
30
  import { playNotificationSound } from './SettingsProvider';
30
31
  import { useSettings } from './SettingsProvider';
@@ -470,7 +471,7 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
470
471
  const currentUserName = currentUser?.displayName.toLowerCase();
471
472
  for (const conversation of relayDMsState.conversations) {
472
473
  for (const participant of conversation.participants) {
473
- const name = typeof participant === 'string' ? participant : participant.agentName;
474
+ const name = getRelayDmParticipantName(participant);
474
475
  if (!name) continue;
475
476
  const lowered = name.toLowerCase();
476
477
  if (currentUserName && lowered === currentUserName) continue;
@@ -508,12 +509,12 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
508
509
  if (!conversation.unreadCount) continue;
509
510
 
510
511
  const match = conversation.participants.find((p) => {
511
- const name = typeof p === 'string' ? p : p.agentName;
512
+ const name = getRelayDmParticipantName(p);
512
513
  if (!name) return false;
513
514
  const lowered = name.toLowerCase();
514
515
  return lowered !== currentUserName && !agentNames.has(lowered);
515
516
  });
516
- const participantName = match ? (typeof match === 'string' ? match : match.agentName) : null;
517
+ const participantName = match ? getRelayDmParticipantName(match) : null;
517
518
 
518
519
  if (participantName) {
519
520
  counts[participantName] = (counts[participantName] || 0) + conversation.unreadCount;
@@ -9,6 +9,7 @@ interface RelayConfigResponse {
9
9
  apiKey?: string;
10
10
  agentToken?: string;
11
11
  agentName?: string | null;
12
+ channels?: string[];
12
13
  }
13
14
 
14
15
  export interface RelayConfigProviderProps {
@@ -31,6 +32,9 @@ export function useRelayConfigStatus(): RelayConfigStatus {
31
32
  return useContext(RelayConfigStatusContext);
32
33
  }
33
34
 
35
+ /** Default channels the dashboard agent should subscribe to via WebSocket */
36
+ const DEFAULT_CHANNELS = ['general'];
37
+
34
38
  export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
35
39
  const [config, setConfig] = useState<RelayConfigResponse | null>(null);
36
40
  const [loaded, setLoaded] = useState(false);
@@ -79,12 +83,23 @@ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
79
83
  };
80
84
  }, [configured, config]);
81
85
 
86
+ // Channels to auto-subscribe on WebSocket connect/reconnect.
87
+ const channels = useMemo(() => {
88
+ if (!configured) return undefined;
89
+ const serverChannels = config?.channels;
90
+ if (Array.isArray(serverChannels) && serverChannels.length > 0) {
91
+ return serverChannels;
92
+ }
93
+ return DEFAULT_CHANNELS;
94
+ }, [configured, config?.channels]);
95
+
82
96
  return (
83
97
  <RelayConfigStatusContext.Provider value={{ configured, loading: !loaded, agentName: config?.agentName ?? null }}>
84
98
  <RelayProvider
85
99
  baseUrl={providerConfig.baseUrl}
86
100
  apiKey={providerConfig.apiKey}
87
101
  agentToken={providerConfig.agentToken}
102
+ channels={channels}
88
103
  >
89
104
  {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
90
105
  {children as any}
@@ -156,9 +156,18 @@ export function SendProvider({ children, localMessages, localUsername }: SendPro
156
156
  }, [localMessages, selectedChannelId, effectiveActiveWorkspaceId, currentUser?.displayName, relayAgentName]);
157
157
 
158
158
  const effectiveChannelMessages = useMemo(() => {
159
- const sourceMessages = usingRelayChannelMessages
160
- ? relayMappedChannelMessages
161
- : (channelMessages.length > 0 ? channelMessages : localChannelMessages);
159
+ if (usingRelayChannelMessages) {
160
+ // Prefer relay SDK messages; fall back to server-fetched messages when
161
+ // the relay hook returns empty (e.g. agent hasn't joined the channel yet).
162
+ if (relayMappedChannelMessages.length > 0) {
163
+ return sortChannelMessagesChronologically(relayMappedChannelMessages);
164
+ }
165
+ if (channelMessages.length > 0) {
166
+ return sortChannelMessagesChronologically(channelMessages);
167
+ }
168
+ return [];
169
+ }
170
+ const sourceMessages = channelMessages.length > 0 ? channelMessages : localChannelMessages;
162
171
  return sortChannelMessagesChronologically(sourceMessages);
163
172
  }, [usingRelayChannelMessages, relayMappedChannelMessages, channelMessages, localChannelMessages]);
164
173
 
@@ -252,7 +261,12 @@ export function SendProvider({ children, localMessages, localUsername }: SendPro
252
261
  if (isWorkspaceFeaturesEnabled && !effectiveActiveWorkspaceId) return;
253
262
 
254
263
  if (relayConfigured && selectedChannelId.startsWith('#')) {
255
- setChannelMessages(relayMappedChannelMessages);
264
+ // When the relay SDK has messages, use them directly.
265
+ // Otherwise, keep any server-fetched messages we already have — do NOT
266
+ // overwrite them with the empty relay array on every re-render.
267
+ if (relayMappedChannelMessages.length > 0) {
268
+ setChannelMessages(relayMappedChannelMessages);
269
+ }
256
270
  setHasMoreMessages(false);
257
271
  setChannelUnreadState(undefined);
258
272
  setChannelsList(prev =>
@@ -260,6 +274,26 @@ export function SendProvider({ children, localMessages, localUsername }: SendPro
260
274
  c.id === selectedChannelId ? { ...c, unreadCount: 0, hasMentions: false } : c
261
275
  )
262
276
  );
277
+
278
+ // Fallback: when the relay SDK hook returns empty (e.g. dashboard agent
279
+ // hasn't joined the channel), fetch from the server API which uses the
280
+ // workspace API key and has full read access to all channels.
281
+ if (relayMappedChannelMessages.length === 0 && !relayMessagesState.loading && !fetchedChannelsRef.current.has(selectedChannelId)) {
282
+ const channelToFetch = selectedChannelId;
283
+ fetchedChannelsRef.current.add(channelToFetch);
284
+ (async () => {
285
+ try {
286
+ const { getMessages } = await import('../components/channels');
287
+ const response = await getMessages(effectiveActiveWorkspaceId || 'local', channelToFetch, { limit: 200 });
288
+ const sortedMessages = sortChannelMessagesChronologically(response.messages);
289
+ setChannelMessageMap(prev => ({ ...prev, [channelToFetch]: sortedMessages }));
290
+ setChannelMessages(sortedMessages);
291
+ } catch (err) {
292
+ console.error('Failed to fetch channel messages fallback:', err);
293
+ fetchedChannelsRef.current.delete(channelToFetch);
294
+ }
295
+ })();
296
+ }
263
297
  return;
264
298
  }
265
299