mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
fix(mobile): deliver alerts despite desktop focus
This commit is contained in:
@@ -899,7 +899,7 @@ describe('registerNotificationHandlers', () => {
|
||||
expect(dispatchMobileNotification).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not dispatch mobile notifications for focused active-worktree notifications', async () => {
|
||||
it('dispatches one mobile notification when the active worktree is focused on desktop', async () => {
|
||||
getAllWindowsMock.mockReturnValue([
|
||||
{
|
||||
isDestroyed: () => false,
|
||||
@@ -922,17 +922,25 @@ describe('registerNotificationHandlers', () => {
|
||||
)
|
||||
|
||||
const handler = getDispatchHandler()
|
||||
expect(
|
||||
await handler(
|
||||
{},
|
||||
{ source: 'agent-task-complete', worktreeId: 'repo::wt1', isActiveWorktree: true }
|
||||
)
|
||||
).toEqual({
|
||||
const focusedNotification = {
|
||||
source: 'agent-task-complete' as const,
|
||||
worktreeId: 'repo::wt1',
|
||||
isActiveWorktree: true
|
||||
}
|
||||
expect(await handler({}, focusedNotification)).toEqual({
|
||||
delivered: false,
|
||||
reason: 'suppressed-focus'
|
||||
})
|
||||
expect(await handler({}, focusedNotification)).toEqual({
|
||||
delivered: false,
|
||||
reason: 'suppressed-focus'
|
||||
})
|
||||
|
||||
expect(dispatchMobileNotification).not.toHaveBeenCalled()
|
||||
expect(dispatchMobileNotification).toHaveBeenCalledTimes(1)
|
||||
expect(dispatchMobileNotification).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ source: 'agent-task-complete', worktreeId: 'repo::wt1' })
|
||||
)
|
||||
expect(notificationCtorMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not dispatch mobile notifications for cooldown-suppressed bursts', async () => {
|
||||
|
||||
@@ -312,8 +312,24 @@ function pruneRecentNotifications(recentNotifications: Map<string, number>, now:
|
||||
}
|
||||
}
|
||||
|
||||
function reserveNotificationCooldown(
|
||||
recentNotifications: Map<string, number>,
|
||||
dedupeKey: string,
|
||||
now: number
|
||||
): boolean {
|
||||
const lastSentAt = recentNotifications.get(dedupeKey) ?? 0
|
||||
if (now - lastSentAt < NOTIFICATION_COOLDOWN_MS) {
|
||||
return false
|
||||
}
|
||||
recentNotifications.delete(dedupeKey)
|
||||
recentNotifications.set(dedupeKey, now)
|
||||
pruneRecentNotifications(recentNotifications, now)
|
||||
return true
|
||||
}
|
||||
|
||||
export function registerNotificationHandlers(store: Store, runtime?: OrcaRuntimeService): void {
|
||||
const recentNotifications = new Map<string, number>()
|
||||
const recentDesktopNotifications = new Map<string, number>()
|
||||
const recentMobileNotifications = new Map<string, number>()
|
||||
// Why: handler registration marks a fresh session — permission evidence
|
||||
// from a previous registration must not leak into the new one.
|
||||
lastObservedDeliveryOutcome = null
|
||||
@@ -441,6 +457,24 @@ export function registerNotificationHandlers(store: Store, runtime?: OrcaRuntime
|
||||
return { delivered: false, reason: 'source-disabled' }
|
||||
}
|
||||
|
||||
const notificationOptions = buildNotificationOptions(args)
|
||||
|
||||
// Why: desktop focus only means this computer has the worktree visible;
|
||||
// the paired phone may be locked or elsewhere and still needs the alert.
|
||||
if (runtime && args.source !== 'test') {
|
||||
const dedupeKey = args.worktreeId ?? args.worktreeLabel ?? 'global'
|
||||
if (reserveNotificationCooldown(recentMobileNotifications, dedupeKey, Date.now())) {
|
||||
runtime.dispatchMobileNotification({
|
||||
type: 'notification',
|
||||
source: args.source,
|
||||
title: notificationOptions.title,
|
||||
body: notificationOptions.body,
|
||||
worktreeId: args.worktreeId,
|
||||
...(args.notificationId ? { notificationId: args.notificationId } : {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const browserWindow =
|
||||
BrowserWindow.getAllWindows().find((window) => !window.isDestroyed()) ?? null
|
||||
if (
|
||||
@@ -458,33 +492,9 @@ export function registerNotificationHandlers(store: Store, runtime?: OrcaRuntime
|
||||
// Dedupe by worktree, not by source — an agent finishing and a terminal bell
|
||||
// often fire within the same data chunk so only the first one should surface.
|
||||
const dedupeKey = args.worktreeId ?? args.worktreeLabel ?? 'global'
|
||||
const now = Date.now()
|
||||
const lastSentAt = recentNotifications.get(dedupeKey) ?? 0
|
||||
if (now - lastSentAt < NOTIFICATION_COOLDOWN_MS) {
|
||||
if (!reserveNotificationCooldown(recentDesktopNotifications, dedupeKey, Date.now())) {
|
||||
return { delivered: false, reason: 'cooldown' }
|
||||
}
|
||||
recentNotifications.delete(dedupeKey)
|
||||
recentNotifications.set(dedupeKey, now)
|
||||
|
||||
// Why: a storm across many worktrees should not make every
|
||||
// notification dispatch scan an ever-growing cooldown table.
|
||||
pruneRecentNotifications(recentNotifications, now)
|
||||
}
|
||||
|
||||
const notificationOptions = buildNotificationOptions(args)
|
||||
|
||||
// Why: paired mobile clients should follow the same user-facing
|
||||
// notification gates as desktop delivery, while still working on hosts
|
||||
// where Electron native notifications are unavailable.
|
||||
if (runtime && args.source !== 'test') {
|
||||
runtime.dispatchMobileNotification({
|
||||
type: 'notification',
|
||||
source: args.source,
|
||||
title: notificationOptions.title,
|
||||
body: notificationOptions.body,
|
||||
worktreeId: args.worktreeId,
|
||||
...(args.notificationId ? { notificationId: args.notificationId } : {})
|
||||
})
|
||||
}
|
||||
|
||||
if (!Notification.isSupported()) {
|
||||
|
||||
Reference in New Issue
Block a user