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
43 changes: 40 additions & 3 deletions src/renderer/src/components/broker/BrokerDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
Check,
Cloud,
Copy,
ExternalLink,
KeyRound,
List,
MessageSquare,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Build observer links from the configured cloud host

When Pear is running against a non-production cloud/relay deployment (RELAY_CLOUD_URL or a saved auth apiUrl; see src/main/auth.ts:getApiUrl), the workspace key belongs to that deployment, but this hard-coded production URL sends the Join flow and key to agentrelay.com, so observer joins either fail or use the wrong environment. Derive the observer origin from the configured cloud URL or include that URL in BrokerDetails before rendering the link.

Useful? React with 👍 / 👎.

}

function getPrimaryRelaycastWorkspace(
broker: BrokerDetails
): NonNullable<BrokerDetails['relaycast']>['workspaces'][number] | undefined {
Expand Down Expand Up @@ -377,28 +383,54 @@ function DetailField({
function CompactMetaRow({
label,
value,
copyValue
copyValue,
action
}: {
label: string
value: string
copyValue?: string
action?: React.ReactNode
}): React.ReactNode {
return (
<div className="grid min-w-0 grid-cols-[126px_minmax(0,1fr)_auto] items-center gap-2 border-b border-[var(--pear-border-subtle)] py-2 last:border-b-0">
<span className="text-[11px] text-[var(--pear-text-faint)]">{label}</span>
<span className="min-w-0 truncate font-mono text-[12px] text-[var(--pear-text-secondary)]" title={value}>
{value}
</span>
{copyValue ? <CopyButton value={copyValue} label="Copy" /> : <span />}
{copyValue || action ? (
<span className="inline-flex shrink-0 items-center gap-1.5">
{action}
{copyValue ? <CopyButton value={copyValue} label="Copy" /> : null}
</span>
) : (
<span />
)}
</div>
)
}

function ObserverLink({ href }: { href: string }): React.ReactNode {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="inline-flex h-7 shrink-0 items-center gap-1.5 rounded-md border border-[var(--pear-border-subtle)] px-2 text-[11px] text-[var(--pear-text-dim)] hover:border-[var(--pear-accent-dim)] hover:bg-[var(--pear-bg-surface-hover)] hover:text-[var(--pear-text)]"
title="Join as observer"
aria-label="Join workspace as observer"
>
<ExternalLink size={12} />
<span>Join</span>
</a>
)
}

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}`
Expand All @@ -423,7 +455,12 @@ function BrokerMetadataSummary({ broker }: { broker: BrokerDetails }): React.Rea
value={compactValue(workspaceAlias ? `${workspaceAlias} (${workspaceId || 'no id'})` : workspaceId)}
copyValue={workspaceId}
/>
<CompactMetaRow label="Workspace key" value={compactValue(workspaceKey)} copyValue={workspaceKey} />
<CompactMetaRow
label="Workspace key"
value={compactValue(workspaceKey)}
copyValue={workspaceKey}
action={observerUrl ? <ObserverLink href={observerUrl} /> : undefined}
/>
<CompactMetaRow label="Self agent" value={selfAgent} copyValue={primaryWorkspace?.selfAgentId} />
<CompactMetaRow
label="Runtime"
Expand Down
14 changes: 14 additions & 0 deletions src/renderer/src/hooks/use-message-reconciliation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ describe('getActiveMessageReconciliationRequest', () => {
})).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: {
Expand Down Expand Up @@ -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/)
})
})
15 changes: 15 additions & 0 deletions src/renderer/src/hooks/use-message-reconciliation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down
Loading