Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 71 additions & 16 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
],
"scripts": {
"dev": "concurrently \"npm run dev -w @agent-relay/dashboard\" \"npm run dev -w @agent-relay/dashboard-server\"",
"dev:relay": "npm run build && cd ../relay && node dist/src/cli/bootstrap.js up --verbose",
"dev:mock": "concurrently \"npm run dev -w @agent-relay/dashboard\" \"npm run dev:mock -w @agent-relay/dashboard-server\"",
"dev:mock:cloud": "concurrently \"NEXT_PUBLIC_FORCE_CLOUD_MODE=true npm run dev -w @agent-relay/dashboard\" \"npm run dev:mock -w @agent-relay/dashboard-server\"",
"build": "npm run build -w @agent-relay/dashboard-server && npm run build -w @agent-relay/dashboard && rm -rf packages/dashboard-server/out && cp -r packages/dashboard/out packages/dashboard-server/out",
Expand Down
8 changes: 4 additions & 4 deletions packages/dashboard-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"@agent-relay/config": "^2.4.7",
"@agent-relay/config": "^3.1.11",
"@agent-relay/protocol": "^2.3.14",
"@agent-relay/sdk": "^3.0.1",
"@agent-relay/sdk": "^3.1.11",
"@agent-relay/storage": "^2.3.14",
"@agent-relay/trajectory": "^2.4.7",
"@agent-relay/utils": "^2.4.7",
"@agent-relay/trajectory": "^3.1.11",
"@agent-relay/utils": "^3.1.11",
"@relaycast/sdk": "^0.5.0",
"@relaycast/types": "^0.5.0",
"express": "^5.2.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard-server/src/proxy-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ export function createServer(options: DashboardServerOptions = {}): DashboardSer
const startupConfig = resolveRelaycastConfig();
if (startupConfig?.apiKey) {
console.log(`[dashboard] Relaycast workspace key: ${startupConfig.apiKey}`);
console.log('[dashboard] View messages at https://app.relaycast.dev');
console.log('[dashboard] View messages at https://agentrelay.dev/observer');
}

registerAgentRoutes(app, ctx);
Expand Down Expand Up @@ -574,7 +574,7 @@ async function bootstrapRelayApiKeyFromBroker(
if (key) {
setRelayApiKey(key);
console.log(`[dashboard] Relaycast workspace key: ${key}`);
console.log('[dashboard] View messages at https://app.relaycast.dev');
console.log('[dashboard] View messages at https://agentrelay.dev/observer');
return;
}
} catch {
Expand Down
1 change: 1 addition & 0 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"dependencies": {
"@nangohq/frontend": "^0.69.20",
"@relaycast/react": "^0.5.0",
"@relaycast/sdk": "^0.5.0",
"@relaycast/types": "^0.5.0",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-search": "^0.16.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/dashboard/src/components/ChannelChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export function ChannelChat({

const isDm = channel.startsWith('dm:');
const channelDisplay = isDm
? channel.split(':').slice(1).filter(u => u !== currentUser).join(', ')
? channel.split(':').slice(1).filter(u => u !== currentUser).join(' ')
: channel;

// Convert online user presence to HumanUser format for mentions
Expand Down
4 changes: 2 additions & 2 deletions packages/dashboard/src/components/ChannelSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,12 +388,12 @@ function ChannelItem({ channel, displayName, isSelected, unreadCount, onSelect,

/**
* Format DM channel name for display.
* dm:alice:bob -> "alice, bob" (excluding current user if known)
* dm:alice:bob -> "alicebob"
*/
function formatDmName(channel: string): string {
if (!channel.startsWith('dm:')) return channel;
const parts = channel.split(':').slice(1);
return parts.join(', ');
return parts.join(' ');
}

export default ChannelSidebar;
31 changes: 30 additions & 1 deletion packages/dashboard/src/lib/relaycastMessageAdapters.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { Message } from '../types';
import { normalizeRelayDmMessageTargets } from './relaycastMessageAdapters.js';
import { getRelayDmParticipantName, normalizeRelayDmMessageTargets } from './relaycastMessageAdapters.js';

function setRelayUsername(value?: string): void {
const storage = (globalThis as { localStorage?: Storage }).localStorage;
Expand Down Expand Up @@ -93,6 +93,35 @@ describe('normalizeRelayDmMessageTargets', () => {
expect(normalized[0]?.to).toBe('Natty');
});

it('maps dm_* targets to object participants using agent_name', () => {
const messages: Message[] = [
{
id: 'msg-2a',
from: 'Natty',
to: 'dm_7b62c72644b9316e7e10a992',
content: 'hello',
timestamp: '2026-02-24T12:00:06.000Z',
},
];

const normalized = normalizeRelayDmMessageTargets(messages, [
{
id: 'dm_7b62c72644b9316e7e10a992',
participants: [{ agent_name: 'Natty' }, { agent_name: 'test-broker-new' }],
},
]);

expect(normalized[0]?.to).toBe('test-broker-new');
});

it('normalizes participant names using getRelayDmParticipantName', () => {
expect(getRelayDmParticipantName({ agent_name: 'Lead', name: 'ignored' })).toBe('Lead');
expect(getRelayDmParticipantName({ agentName: 'Codex-Worker' })).toBe('Codex-Worker');
expect(getRelayDmParticipantName('Test-Broker')).toBe('Test-Broker');
expect(getRelayDmParticipantName({ username: 'human-user' })).toBe('human-user');
expect(getRelayDmParticipantName(123)).toBeNull();
});

it('leaves non-dm and unknown dm targets unchanged', () => {
const messages: Message[] = [
{
Expand Down
35 changes: 28 additions & 7 deletions packages/dashboard/src/lib/relaycastMessageAdapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ type RelayDmConversationLike = {
participants: unknown[];
};

export function getRelayDmParticipantName(participant: unknown): string | null {
if (typeof participant === 'string') {
return normalizeRelayIdentity(participant);
}

if (participant === null || typeof participant !== 'object') {
return null;
}

const record = participant as Record<string, unknown>;
const rawName = (
record.agent_name
?? record.agentName
?? record.name
?? record.username
);

if (typeof rawName !== 'string') {
return null;
}

return normalizeRelayIdentity(rawName);
}

function normalizeRelayIdentity(value: string): string {
const trimmed = value.trim();
if (!trimmed) return '';
Expand Down Expand Up @@ -39,20 +63,17 @@ function resolveDmRecipient(
const senderKey = normalizeRelayIdentity(sender).toLowerCase();

for (const participant of participants) {
if (typeof participant !== 'string') continue;
const normalized = normalizeRelayIdentity(participant);
const normalized = getRelayDmParticipantName(participant);
if (!normalized) continue;
if (normalized.toLowerCase() !== senderKey) {
return normalized;
}
}

for (const participant of participants) {
if (typeof participant !== 'string') continue;
const normalized = normalizeRelayIdentity(participant);
if (normalized) {
return normalized;
}
const normalized = getRelayDmParticipantName(participant);
if (!normalized) continue;
return normalized;
}

return null;
Expand Down
19 changes: 15 additions & 4 deletions packages/dashboard/src/providers/ChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,20 @@ export function ChannelProvider({ children }: ChannelProviderProps) {

// Relay channel state
const relayChannelsState = useRelayChannels();
const relayChannelsLoading = relayChannelsState.loading;
const relayChannelsRaw = relayChannelsState.channels;

// Stabilize the mapped channels array — only recompute when the serialized
// channel list actually changes (avoids infinite re-render loops from new
// array references returned by the relay hook on every render).
const relayChannelsKey = useMemo(
() => JSON.stringify(relayChannelsRaw.map(c => c.name + ':' + (c.topic ?? '') + ':' + (c.isArchived ?? false) + ':' + (c.memberCount ?? 0))),
[relayChannelsRaw],
);
const relayMappedChannels = useMemo(
() => relayChannelsState.channels.map(mapRelayChannelToDashboard),
[relayChannelsState.channels],
() => relayChannelsRaw.map(mapRelayChannelToDashboard),
// eslint-disable-next-line react-hooks/exhaustive-deps
[relayChannelsKey],
);

// Channel list state
Expand Down Expand Up @@ -225,7 +236,7 @@ export function ChannelProvider({ children }: ChannelProviderProps) {
const activeChannels = relayMappedChannels.filter((channel) => channel.status !== 'archived');
const archivedChannels = relayMappedChannels.filter((channel) => channel.status === 'archived');
setChannelListsFromResponse({ channels: activeChannels, archivedChannels });
setIsChannelsLoading(relayChannelsState.loading);
setIsChannelsLoading(relayChannelsLoading);
return;
}

Expand Down Expand Up @@ -253,7 +264,7 @@ export function ChannelProvider({ children }: ChannelProviderProps) {
fetchChannels();
}, [
relayConfigured,
relayChannelsState,
relayChannelsLoading,
relayMappedChannels,
effectiveActiveWorkspaceId,
isWorkspaceFeaturesEnabled,
Expand Down
7 changes: 4 additions & 3 deletions packages/dashboard/src/providers/MessageProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useRelayConfigStatus } from './RelayConfigProvider';
import { isDashboardVariant } from '../lib/identity';
import {
normalizeRelayDmMessageTargets,
getRelayDmParticipantName,
} from '../lib/relaycastMessageAdapters';
import { playNotificationSound } from './SettingsProvider';
import { useSettings } from './SettingsProvider';
Expand Down Expand Up @@ -470,7 +471,7 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
const currentUserName = currentUser?.displayName.toLowerCase();
for (const conversation of relayDMsState.conversations) {
for (const participant of conversation.participants) {
const name = typeof participant === 'string' ? participant : participant.agentName;
const name = getRelayDmParticipantName(participant);
if (!name) continue;
const lowered = name.toLowerCase();
if (currentUserName && lowered === currentUserName) continue;
Expand Down Expand Up @@ -508,12 +509,12 @@ function MessageProviderInner({ children, data, rawData: _rawData, enableReactio
if (!conversation.unreadCount) continue;

const match = conversation.participants.find((p) => {
const name = typeof p === 'string' ? p : p.agentName;
const name = getRelayDmParticipantName(p);
if (!name) return false;
const lowered = name.toLowerCase();
return lowered !== currentUserName && !agentNames.has(lowered);
});
const participantName = match ? (typeof match === 'string' ? match : match.agentName) : null;
const participantName = match ? getRelayDmParticipantName(match) : null;

if (participantName) {
counts[participantName] = (counts[participantName] || 0) + conversation.unreadCount;
Expand Down
Loading