@agent-relay/dashboard 2.0.87 → 2.0.89

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 (74) hide show
  1. package/out/404.html +1 -1
  2. package/out/_next/static/chunks/270-8c0b8109123a0c5f.js +73 -0
  3. package/out/_next/static/chunks/{1028-85d028e818d3ef30.js → 9626-10e71fc51b892784.js} +1 -1
  4. package/out/_next/static/chunks/app/app/[[...slug]]/{page-f33c6214e21ccfdd.js → page-4f01a33b51f23cea.js} +1 -1
  5. package/out/_next/static/chunks/app/{page-2e38080856c3c293.js → page-2644ed4067d978e0.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/components/hooks/useAllDMs.ts +88 -0
  66. package/src/components/hooks/useDirectMessage.ts +12 -0
  67. package/src/lib/relaycastMessageAdapters.test.ts +30 -1
  68. package/src/lib/relaycastMessageAdapters.ts +28 -7
  69. package/src/providers/ChannelProvider.tsx +15 -4
  70. package/src/providers/MessageProvider.tsx +20 -21
  71. package/src/providers/RelayConfigProvider.tsx +0 -2
  72. package/out/_next/static/chunks/3238-24c1e4b1cefe3c71.js +0 -73
  73. /package/out/_next/static/{Z91dsVnsvqDCvvYfCXRXH → SiFRCTvfNgk02ZX5qxVBQ}/_buildManifest.js +0 -0
  74. /package/out/_next/static/{Z91dsVnsvqDCvvYfCXRXH → SiFRCTvfNgk02ZX5qxVBQ}/_ssgManifest.js +0 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * useAllDMs Hook
3
+ *
4
+ * Fetches ALL DM conversations in the workspace using the workspace-level API
5
+ * (`allDmConversations`). Unlike the SDK's built-in `useDMs` which only returns
6
+ * conversations the dashboard agent participates in, this hook returns agent-to-agent
7
+ * DMs as well, making them visible in the DM sidebar.
8
+ *
9
+ * Listens for `dm.received` and `group_dm.received` WebSocket events to
10
+ * automatically refetch when new DMs arrive.
11
+ */
12
+
13
+ import { useCallback, useEffect, useRef, useState } from 'react';
14
+ import { useRelay, useEvent } from '@relaycast/react';
15
+ import { useRelayConfigStatus } from '../../providers/RelayConfigProvider';
16
+
17
+ export interface AllDmConversation {
18
+ id: string;
19
+ channelId?: string;
20
+ type: string;
21
+ participants: string[];
22
+ lastMessage: {
23
+ text: string;
24
+ agentName: string;
25
+ createdAt: string;
26
+ } | null;
27
+ messageCount: number;
28
+ unreadCount?: number;
29
+ }
30
+
31
+ interface UseAllDMsResult {
32
+ conversations: AllDmConversation[];
33
+ loading: boolean;
34
+ error: Error | null;
35
+ refetch: () => void;
36
+ }
37
+
38
+ export function useAllDMs(): UseAllDMsResult {
39
+ const { configured } = useRelayConfigStatus();
40
+ const [conversations, setConversations] = useState<AllDmConversation[]>([]);
41
+ const [loading, setLoading] = useState(true);
42
+ const [error, setError] = useState<Error | null>(null);
43
+ const fetchingRef = useRef(false);
44
+
45
+ // useRelay() throws if not inside RelayProvider, but we always are.
46
+ // When relay is not configured, the provider uses dummy credentials and
47
+ // allDmConversations will fail — we guard with `configured`.
48
+ let relay: ReturnType<typeof useRelay> | null = null;
49
+ try {
50
+ relay = useRelay();
51
+ } catch {
52
+ // Not inside RelayProvider — relay stays null
53
+ }
54
+
55
+ const fetchConversations = useCallback(async () => {
56
+ if (!configured || !relay || fetchingRef.current) return;
57
+ fetchingRef.current = true;
58
+ try {
59
+ const data = await relay.allDmConversations();
60
+ setConversations(Array.isArray(data) ? data as AllDmConversation[] : []);
61
+ setError(null);
62
+ } catch (err) {
63
+ setError(err instanceof Error ? err : new Error(String(err)));
64
+ } finally {
65
+ setLoading(false);
66
+ fetchingRef.current = false;
67
+ }
68
+ }, [configured, relay]);
69
+
70
+ // Initial fetch
71
+ useEffect(() => {
72
+ if (!configured) {
73
+ setLoading(false);
74
+ return;
75
+ }
76
+ void fetchConversations();
77
+ }, [configured, fetchConversations]);
78
+
79
+ // Refetch when DM events arrive via WebSocket
80
+ useEvent('dm.received', () => {
81
+ void fetchConversations();
82
+ });
83
+ useEvent('group_dm.received', () => {
84
+ void fetchConversations();
85
+ });
86
+
87
+ return { conversations, loading, error, refetch: fetchConversations };
88
+ }
@@ -41,10 +41,22 @@ export function useDirectMessage({
41
41
  for (const msg of messages) {
42
42
  const { from, to } = msg;
43
43
  if (!from || !to) continue;
44
+ // Messages involving the selected human
44
45
  if (from === humanName && agentNameSet.has(to)) derived.add(to);
45
46
  if (to === humanName && agentNameSet.has(from)) derived.add(from);
46
47
  if (selectedDmAgents.includes(from) && agentNameSet.has(to)) derived.add(to);
47
48
  if (selectedDmAgents.includes(to) && agentNameSet.has(from)) derived.add(from);
49
+ // Include agents from DM messages (non-channel messages) so agent-to-agent
50
+ // DMs are visible in the DM view — but only if the message involves the
51
+ // current human or an already-selected DM agent to avoid cross-conversation leakage.
52
+ if (!to.startsWith('#')) {
53
+ const involvesHuman = (from === humanName || to === humanName);
54
+ const involvesSelected = selectedDmAgents.includes(from) || selectedDmAgents.includes(to);
55
+ if (involvesHuman || involvesSelected) {
56
+ if (agentNameSet.has(from)) derived.add(from);
57
+ if (agentNameSet.has(to)) derived.add(to);
58
+ }
59
+ }
48
60
  }
49
61
 
50
62
  const participants = new Set<string>([...selectedDmAgents, ...derived]);
@@ -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,
@@ -12,19 +12,18 @@
12
12
  import React, { createContext, useContext, useState, useCallback, useEffect, useMemo, useRef } from 'react';
13
13
  import type { Message } from '../types';
14
14
  import type { HumanUser } from '../components/MentionAutocomplete';
15
- import {
16
- useDMs as useRelayDMs,
17
- } from '@relaycast/react';
18
15
  import { useMessages as useMessagesHook } from '../components/hooks/useMessages';
19
16
  import { useThread } from '../components/hooks/useThread';
20
17
  import { usePresence, type UserPresence } from '../components/hooks/usePresence';
21
18
  import { useDirectMessage } from '../components/hooks/useDirectMessage';
19
+ import { useAllDMs } from '../components/hooks/useAllDMs';
22
20
  import { useCloudWorkspace } from './CloudWorkspaceProvider';
23
21
  import { useAgentContext } from './AgentProvider';
24
22
  import { useRelayConfigStatus } from './RelayConfigProvider';
25
23
  import { isDashboardVariant } from '../lib/identity';
26
24
  import {
27
25
  normalizeRelayDmMessageTargets,
26
+ getRelayDmParticipantName,
28
27
  } from '../lib/relaycastMessageAdapters';
29
28
  import { playNotificationSound } from './SettingsProvider';
30
29
  import { useSettings } from './SettingsProvider';
@@ -377,14 +376,14 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
377
376
  // Relay DMs and message normalization
378
377
  // ---------------------------------------------------------------------------
379
378
 
380
- const relayDMsState = useRelayDMs();
379
+ const allDMsState = useAllDMs();
381
380
  const normalizedRelayMessages = useMemo(() => {
382
381
  const sourceMessages = data?.messages ?? [];
383
- if (!relayConfigured || relayDMsState.conversations.length === 0) {
382
+ if (!relayConfigured || allDMsState.conversations.length === 0) {
384
383
  return sourceMessages;
385
384
  }
386
- return normalizeRelayDmMessageTargets(sourceMessages, relayDMsState.conversations);
387
- }, [data?.messages, relayConfigured, relayDMsState.conversations]);
385
+ return normalizeRelayDmMessageTargets(sourceMessages, allDMsState.conversations);
386
+ }, [data?.messages, relayConfigured, allDMsState.conversations]);
388
387
 
389
388
  // ---------------------------------------------------------------------------
390
389
  // Core message hook
@@ -441,7 +440,7 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
441
440
  const { visibleMessages: dedupedVisibleMessages, participantAgents: dmParticipantAgents } = useDirectMessage({
442
441
  currentHuman,
443
442
  currentUserName: currentUser?.displayName ?? null,
444
- messages,
443
+ messages: currentHuman ? normalizedRelayMessages : messages,
445
444
  agents,
446
445
  selectedDmAgents,
447
446
  removedDmAgents,
@@ -466,11 +465,11 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
466
465
  });
467
466
  }
468
467
 
469
- if (relayConfigured && relayDMsState.conversations.length > 0) {
468
+ if (relayConfigured && allDMsState.conversations.length > 0) {
470
469
  const currentUserName = currentUser?.displayName.toLowerCase();
471
- for (const conversation of relayDMsState.conversations) {
470
+ for (const conversation of allDMsState.conversations) {
472
471
  for (const participant of conversation.participants) {
473
- const name = typeof participant === 'string' ? participant : participant.agentName;
472
+ const name = getRelayDmParticipantName(participant);
474
473
  if (!name) continue;
475
474
  const lowered = name.toLowerCase();
476
475
  if (currentUserName && lowered === currentUserName) continue;
@@ -490,7 +489,7 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
490
489
  }
491
490
 
492
491
  return Array.from(seenUsers.values());
493
- }, [normalizedRelayMessages, agents, currentUser, relayDMsState.conversations, relayConfigured, relayAgentName]);
492
+ }, [normalizedRelayMessages, agents, currentUser, allDMsState.conversations, relayConfigured, relayAgentName]);
494
493
 
495
494
  // ---------------------------------------------------------------------------
496
495
  // Human unread counts
@@ -499,21 +498,21 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
499
498
  const humanUnreadCounts = useMemo(() => {
500
499
  if (!currentUser) return {};
501
500
 
502
- if (relayConfigured && relayDMsState.conversations.length > 0) {
501
+ if (relayConfigured && allDMsState.conversations.length > 0) {
503
502
  const counts: Record<string, number> = {};
504
503
  const currentUserName = currentUser.displayName.toLowerCase();
505
504
  const agentNames = new Set(agents.filter((a) => !a.isHuman).map((a) => a.name.toLowerCase()));
506
505
 
507
- for (const conversation of relayDMsState.conversations) {
506
+ for (const conversation of allDMsState.conversations) {
508
507
  if (!conversation.unreadCount) continue;
509
508
 
510
509
  const match = conversation.participants.find((p) => {
511
- const name = typeof p === 'string' ? p : p.agentName;
510
+ const name = getRelayDmParticipantName(p);
512
511
  if (!name) return false;
513
512
  const lowered = name.toLowerCase();
514
513
  return lowered !== currentUserName && !agentNames.has(lowered);
515
514
  });
516
- const participantName = match ? (typeof match === 'string' ? match : match.agentName) : null;
515
+ const participantName = match ? getRelayDmParticipantName(match) : null;
517
516
 
518
517
  if (participantName) {
519
518
  counts[participantName] = (counts[participantName] || 0) + conversation.unreadCount;
@@ -545,7 +544,7 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
545
544
  }
546
545
 
547
546
  return counts;
548
- }, [combinedAgents, currentUser, normalizedRelayMessages, dmSeenAt, relayDMsState.conversations, agents, relayConfigured]);
547
+ }, [combinedAgents, currentUser, normalizedRelayMessages, dmSeenAt, allDMsState.conversations, agents, relayConfigured]);
549
548
 
550
549
  const markDmSeen = useCallback((username: string) => {
551
550
  setDmSeenAt((prev) => {
@@ -838,16 +837,16 @@ function MessageProviderInnerWithSend({ children, data, rawData, enableReactions
838
837
  // Since SendProvider only needs them for local (non-cloud) channel message rendering,
839
838
  // we derive them here at this level too.
840
839
  const { configured: relayConfigured } = useRelayConfigStatus();
841
- const relayDMsState = useRelayDMs();
840
+ const allDMsState = useAllDMs();
842
841
  const { currentUser } = useCloudWorkspace();
843
842
 
844
843
  const normalizedRelayMessages = useMemo(() => {
845
844
  const sourceMessages = data?.messages ?? [];
846
- if (!relayConfigured || relayDMsState.conversations.length === 0) {
845
+ if (!relayConfigured || allDMsState.conversations.length === 0) {
847
846
  return sourceMessages;
848
847
  }
849
- return normalizeRelayDmMessageTargets(sourceMessages, relayDMsState.conversations);
850
- }, [data?.messages, relayConfigured, relayDMsState.conversations]);
848
+ return normalizeRelayDmMessageTargets(sourceMessages, allDMsState.conversations);
849
+ }, [data?.messages, relayConfigured, allDMsState.conversations]);
851
850
 
852
851
  const [localUsername] = useState<string | null>(
853
852
  typeof window !== 'undefined' ? localStorage.getItem('relay_username') : null
@@ -84,8 +84,6 @@ export function RelayConfigProvider({ children }: RelayConfigProviderProps) {
84
84
  }, [configured, config]);
85
85
 
86
86
  // Channels to auto-subscribe on WebSocket connect/reconnect.
87
- // This ensures the dashboard receives real-time messages even after
88
- // WebSocket reconnects (useMessages effect deps don't re-trigger on reconnect).
89
87
  const channels = useMemo(() => {
90
88
  if (!configured) return undefined;
91
89
  const serverChannels = config?.channels;