diff --git a/src/renderer/src/components/broker/BrokerDetailsPage.tsx b/src/renderer/src/components/broker/BrokerDetailsPage.tsx index 3d2dfbd3..c7c4c184 100644 --- a/src/renderer/src/components/broker/BrokerDetailsPage.tsx +++ b/src/renderer/src/components/broker/BrokerDetailsPage.tsx @@ -7,6 +7,7 @@ import { Check, Cloud, Copy, + ExternalLink, KeyRound, List, MessageSquare, @@ -99,6 +100,11 @@ function compactValue(value: string | undefined, fallback = 'n/a'): string { return value?.trim() || fallback } +function observerUrlForWorkspaceKey(workspaceKey: string | undefined): string | undefined { + const key = workspaceKey?.trim() + return key ? `https://agentrelay.com/observer?key=${encodeURIComponent(key)}` : undefined +} + function getPrimaryRelaycastWorkspace( broker: BrokerDetails ): NonNullable['workspaces'][number] | undefined { @@ -377,11 +383,13 @@ function DetailField({ function CompactMetaRow({ label, value, - copyValue + copyValue, + action }: { label: string value: string copyValue?: string + action?: React.ReactNode }): React.ReactNode { return (
@@ -389,16 +397,40 @@ function CompactMetaRow({ {value} - {copyValue ? : } + {copyValue || action ? ( + + {action} + {copyValue ? : null} + + ) : ( + + )}
) } +function ObserverLink({ href }: { href: string }): React.ReactNode { + return ( + + + Join + + ) +} + function BrokerMetadataSummary({ broker }: { broker: BrokerDetails }): React.ReactNode { const apiKey = broker.apiKey || (broker.apiKeyAvailable ? 'stored in connection file' : 'n/a') const primaryWorkspace = getPrimaryRelaycastWorkspace(broker) const workspaceId = broker.relaycast?.defaultWorkspaceId || primaryWorkspace?.workspaceId const workspaceKey = broker.relaycast?.workspaceKey || broker.session?.workspaceKey + const observerUrl = observerUrlForWorkspaceKey(workspaceKey) const workspaceAlias = primaryWorkspace?.workspaceAlias || undefined const selfAgent = primaryWorkspace ? `${primaryWorkspace.selfName} / ${primaryWorkspace.selfAgentId}` @@ -423,7 +455,12 @@ function BrokerMetadataSummary({ broker }: { broker: BrokerDetails }): React.Rea value={compactValue(workspaceAlias ? `${workspaceAlias} (${workspaceId || 'no id'})` : workspaceId)} copyValue={workspaceId} /> - + : undefined} + /> { })).toBeNull() }) + it('polls only visible active chat rooms', () => { + expect(hooks.shouldPollActiveRoom('channel:project-1:general', 'visible')).toBe(true) + expect(hooks.shouldPollActiveRoom('dm:project-1:human|worker', 'visible')).toBe(true) + expect(hooks.shouldPollActiveRoom('none', 'visible')).toBe(false) + expect(hooks.shouldPollActiveRoom('channel:project-1:general', 'hidden')).toBe(false) + }) + it('builds a channel reconciliation request from broker channel message events', () => { expect(hooks.getBrokerEventMessageReconciliationRequest({ event: { @@ -515,4 +522,11 @@ describe('createMessageReconciler', () => { expect(source).toMatch(/scheduleHumanMessageSentReconciliation\([\s\S]*lastHumanMessageSentAt[\s\S]*reconciler/) expect(source).toMatch(/\[lastHumanMessageSentAt,\s*reconciler\]/) }) + + it('wires active chat rooms into periodic reconciliation', () => { + const source = hooks.useMessageReconciliation.toString() + expect(source).toContain('ACTIVE_ROOM_RECONCILE_POLL_MS') + expect(source).toContain('active-room-poll') + expect(source).toMatch(/shouldPollActiveRoom\([\s\S]*activeRoomKey[\s\S]*document\.visibilityState/) + }) }) diff --git a/src/renderer/src/hooks/use-message-reconciliation.ts b/src/renderer/src/hooks/use-message-reconciliation.ts index 8fd831d4..2249a02c 100644 --- a/src/renderer/src/hooks/use-message-reconciliation.ts +++ b/src/renderer/src/hooks/use-message-reconciliation.ts @@ -13,6 +13,7 @@ import type { const DEFAULT_RECONCILE_LIMIT = 50 const DEFAULT_RECONCILE_DEBOUNCE_MS = 750 +export const ACTIVE_ROOM_RECONCILE_POLL_MS = 3_000 const BROKER_CONNECTED_STATUSES = new Set([ 'connected', 'event_stream_connected', @@ -312,6 +313,10 @@ function refreshEventStream(reason: string): void { void broker?.refreshEventStream?.(projectId, reason)?.catch(() => undefined) } +export function shouldPollActiveRoom(activeRoomKey: string, visibilityState: DocumentVisibilityState): boolean { + return activeRoomKey !== 'none' && visibilityState === 'visible' +} + export function useMessageReconciliation(): void { const activeProjectId = useProjectStore((s) => s.activeProjectId) const activeTabId = useUIStore((s) => s.activeTabId) @@ -350,6 +355,16 @@ export function useMessageReconciliation(): void { reconciler.schedule('active-room') }, [activeRoomKey, reconciler]) + useEffect(() => { + if (activeRoomKey === 'none') return + const interval = window.setInterval(() => { + if (shouldPollActiveRoom(activeRoomKey, document.visibilityState)) { + reconciler.schedule('active-room-poll') + } + }, ACTIVE_ROOM_RECONCILE_POLL_MS) + return () => window.clearInterval(interval) + }, [activeRoomKey, reconciler]) + useEffect(() => { if (brokerStatus === 'connected') { refreshEventStream('broker-status')