Skip to content
Open
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
41 changes: 40 additions & 1 deletion apps/desktop/src/app/desktop-controller-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'

import type { SessionInfo } from '@/hermes'

import { sameCronSignature } from './desktop-controller-utils'
import { sameCronSignature, shouldPollMessagingSessions } from './desktop-controller-utils'

const session = (id: string, title: string | null): SessionInfo => ({ id, title }) as SessionInfo

Expand All @@ -29,3 +29,42 @@ describe('sameCronSignature', () => {
expect(sameCronSignature(a, b)).toBe(false)
})
})

describe('shouldPollMessagingSessions', () => {
const base = {
activeIsMessaging: false,
gatewayPlatforms: null,
messagingSessionCount: 0,
messagingViewOpen: false
}

it('is false when nothing indicates messaging is in use', () => {
expect(shouldPollMessagingSessions(base)).toBe(false)
})

it('is true while the messaging view is open', () => {
expect(shouldPollMessagingSessions({ ...base, messagingViewOpen: true })).toBe(true)
})

it('is true when a messaging session is already loaded', () => {
expect(shouldPollMessagingSessions({ ...base, messagingSessionCount: 1 })).toBe(true)
})

it('is true when the active session is a messaging thread', () => {
expect(shouldPollMessagingSessions({ ...base, activeIsMessaging: true })).toBe(true)
})

it('is true when status reports a configured gateway platform', () => {
expect(
shouldPollMessagingSessions({
...base,
gatewayPlatforms: {
telegram: {
state: 'connected',
updated_at: '2026-07-04T00:00:00+00:00'
}
}
})
).toBe(true)
})
})
19 changes: 19 additions & 0 deletions apps/desktop/src/app/desktop-controller-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { SessionInfo } from '@/hermes'
import type { PlatformStatus } from '@/types/hermes'

// Cheap signature compare so a poll only swaps the atom (and re-renders the
// sidebar) when the visible rows actually changed.
Expand All @@ -24,3 +25,21 @@ export function sameCronSignature(a: SessionInfo[], b: SessionInfo[]): boolean {
)
})
}

export function shouldPollMessagingSessions({
activeIsMessaging,
gatewayPlatforms,
messagingSessionCount,
messagingViewOpen
}: {
activeIsMessaging: boolean
gatewayPlatforms?: null | Record<string, PlatformStatus>
messagingSessionCount: number
messagingViewOpen: boolean
}): boolean {
if (messagingViewOpen || activeIsMessaging || messagingSessionCount > 0) {
return true
}

return Object.keys(gatewayPlatforms ?? {}).length > 0
}
25 changes: 16 additions & 9 deletions apps/desktop/src/app/desktop-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import {
} from './chat/right-rail'
import { ChatSidebar } from './chat/sidebar'
import { CommandPalette } from './command-palette'
import { shouldPollMessagingSessions } from './desktop-controller-utils'
import { useGatewayBoot } from './gateway/hooks/use-gateway-boot'
import { useGatewayRequest } from './gateway/hooks/use-gateway-request'
import { useKeybinds } from './hooks/use-keybinds'
Expand Down Expand Up @@ -919,11 +920,24 @@ export function DesktopController() {
}
}, [gatewayState, refreshCronJobs])

// Only keep the broad messaging session poll armed when the user has a
// messaging surface or the already-polled status endpoint reports a platform.
const activeIsMessaging =
!!selectedStoredSessionId &&
isMessagingSource(messagingSessions.find(s => sessionMatchesStoredId(s, selectedStoredSessionId))?.source)

const pollMessagingSessions = shouldPollMessagingSessions({
activeIsMessaging,
gatewayPlatforms: statusSnapshot?.gateway_platforms,
messagingSessionCount: messagingSessions.length,
messagingViewOpen: currentView === 'messaging'
})

// Keep messaging-platform session lists live: inbound Telegram/WeChat/Discord
// turns are written by the gateway, not the desktop websocket, so they won't
// appear without polling.
useEffect(() => {
if (gatewayState !== 'open') {
if (gatewayState !== 'open' || !pollMessagingSessions) {
return
}

Expand All @@ -940,14 +954,7 @@ export function DesktopController() {
window.clearInterval(intervalId)
document.removeEventListener('visibilitychange', tick)
}
}, [gatewayState, refreshMessagingSessions])

// Only the open messaging transcript needs a poll β€” local chats are already
// live over the websocket, so arming a timer for them would just no-op every
// tick. Gate on the active session actually being a messaging source.
const activeIsMessaging =
!!selectedStoredSessionId &&
isMessagingSource(messagingSessions.find(s => sessionMatchesStoredId(s, selectedStoredSessionId))?.source)
}, [gatewayState, pollMessagingSessions, refreshMessagingSessions])

// Keep the currently-viewed messaging transcript live.
useEffect(() => {
Expand Down
Loading