Skip to content

Commit 4614cc3

Browse files
authored
Add agent completion notifications (#198)
1 parent 9b9915c commit 4614cc3

19 files changed

Lines changed: 268 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
77
## [Unreleased]
88

99
### 🚀 Added
10+
- Agent: system notifications now fire when agents finish work and return to standby. (#198)
1011
- Settings: left-sidebar search helps users locate settings and jump directly to the matching section. (#192)
1112
- CLI: add canvas node control commands for creating, listing, reading, deleting, updating supported node types, and focusing nodes or Spaces. (#193)
1213
- Workspace canvas: experimental website window nodes with opt-in settings, shared-session profile modes, snapshot-backed warm/cold lifecycle, and in-canvas navigation handling. (#141)

src/app/preload/index.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import type {
5555
SuggestWorktreeNamesInput,
5656
SuggestWorktreeNamesResult,
5757
SetWindowChromeThemeInput,
58+
ShowSystemNotificationInput,
59+
ShowSystemNotificationResult,
5860
TerminalDataEvent,
5961
TerminalExitEvent,
6062
TerminalSessionMetadataEvent,
@@ -255,6 +257,9 @@ export interface OpenCoveApi {
255257
}
256258
system: {
257259
listFonts: () => Promise<ListSystemFontsResult>
260+
showNotification: (
261+
payload: ShowSystemNotificationInput,
262+
) => Promise<ShowSystemNotificationResult>
258263
}
259264
worker: {
260265
getStatus: () => Promise<WorkerStatusResult>

src/app/preload/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ import type {
5858
SuggestWorktreeNamesInput,
5959
SuggestWorktreeNamesResult,
6060
SetWindowChromeThemeInput,
61+
ShowSystemNotificationInput,
62+
ShowSystemNotificationResult,
6163
TerminalDataEvent,
6264
TerminalExitEvent,
6365
TerminalSessionMetadataEvent,
@@ -453,6 +455,10 @@ const opencoveApi = {
453455
},
454456
system: {
455457
listFonts: (): Promise<ListSystemFontsResult> => invokeIpc(IPC_CHANNELS.systemListFonts),
458+
showNotification: (
459+
payload: ShowSystemNotificationInput,
460+
): Promise<ShowSystemNotificationResult> =>
461+
invokeIpc(IPC_CHANNELS.systemShowNotification, payload),
456462
},
457463
worker: {
458464
getStatus: (): Promise<WorkerStatusResult> => invokeIpc(IPC_CHANNELS.workerGetStatus),

src/app/renderer/browser/browserOpenCoveApi.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import type { ListSystemFontsResult, WorkspaceDirectory } from '@shared/contracts/dto'
1+
import type {
2+
ListSystemFontsResult,
3+
ShowSystemNotificationInput,
4+
ShowSystemNotificationResult,
5+
WorkspaceDirectory,
6+
} from '@shared/contracts/dto'
27
import { BrowserPtyClient } from './BrowserPtyClient'
38
import { invokeBrowserControlSurface } from './browserControlSurface'
49
import type { ControlSurfaceInvokeRequest } from '@shared/contracts/controlSurface'
@@ -340,6 +345,33 @@ export function installBrowserOpenCoveApi(): void {
340345
},
341346
system: {
342347
listFonts: async (): Promise<ListSystemFontsResult> => ({ fonts: [] }),
348+
showNotification: async (
349+
payload: ShowSystemNotificationInput,
350+
): Promise<ShowSystemNotificationResult> => {
351+
if (typeof window === 'undefined' || !('Notification' in window)) {
352+
return { shown: false }
353+
}
354+
355+
if (Notification.permission === 'default') {
356+
await Notification.requestPermission()
357+
}
358+
359+
if (Notification.permission !== 'granted') {
360+
return { shown: false }
361+
}
362+
363+
const title = payload.title.trim()
364+
if (title.length === 0) {
365+
return { shown: false }
366+
}
367+
368+
const notification = new Notification(title, {
369+
body: typeof payload.body === 'string' ? payload.body : undefined,
370+
silent: payload.silent ?? false,
371+
})
372+
void notification
373+
return { shown: true }
374+
},
343375
},
344376
worker: {
345377
getStatus: async () => unsupportedWorkerStatus(),

src/app/renderer/i18n/locales/en.settingsPanel.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,9 +142,14 @@ export const enSettingsPanel = {
142142
},
143143
notifications: {
144144
title: 'Notifications',
145+
systemNotifications: {
146+
enabledLabel: 'System notifications',
147+
enabledHelp:
148+
'Show a native system notification when an agent transitions from working to standby.',
149+
},
145150
agentStandbyBanner: {
146-
enabledLabel: 'Agent standby banner',
147-
enabledHelp: 'Show a top-right banner when an agent transitions from working to standby.',
151+
enabledLabel: 'Top-right banner',
152+
enabledHelp: 'Show an in-app banner when an agent transitions from working to standby.',
148153
contextTitle: 'Banner context',
149154
contextHelp: 'Choose what context chips are shown in the banner.',
150155
showTask: 'Show task',

src/app/renderer/i18n/locales/en.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export const en = {
8484
sidebar: 'Sidebar',
8585
minimap: 'Minimap',
8686
theme: 'Theme',
87-
agentStandbyBanner: 'Agent standby banner',
87+
agentStandbyBanner: 'Top-right banner',
8888
on: 'On',
8989
off: 'Off',
9090
},

src/app/renderer/i18n/locales/zh-CN.settingsPanel.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,13 @@ export const zhCNSettingsPanel = {
141141
},
142142
notifications: {
143143
title: '通知',
144+
systemNotifications: {
145+
enabledLabel: '系统通知',
146+
enabledHelp: '当 Agent 从工作状态变为待命时,显示原生系统通知。',
147+
},
144148
agentStandbyBanner: {
145-
enabledLabel: 'Agent 完成提醒',
146-
enabledHelp: '当 Agent 从工作状态变为待命时,在右上角显示提醒横幅。',
149+
enabledLabel: '右上角横幅',
150+
enabledHelp: '当 Agent 从工作状态变为待命时,显示应用内右上角横幅。',
147151
contextTitle: '横幅信息',
148152
contextHelp: '控制右上角提醒横幅中显示的上下文信息。',
149153
showTask: '显示任务',

src/app/renderer/i18n/locales/zh-CN.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export const zhCN = {
8686
sidebar: '侧边栏',
8787
minimap: '缩略图',
8888
theme: '主题',
89-
agentStandbyBanner: 'Agent 完成提醒',
89+
agentStandbyBanner: '右上角横幅',
9090
on: '已开启',
9191
off: '已关闭',
9292
},

src/app/renderer/shell/hooks/useAgentStandbyNotifications.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2+
import { useTranslation } from '@app/renderer/i18n'
23
import type { AgentStandbyNotification } from '../components/AppNotifications'
34
import type { GitHubPullRequestSummary, GitWorktreeInfo } from '@shared/contracts/dto'
45
import type { WorkspaceState } from '@contexts/workspace/presentation/renderer/types'
@@ -116,6 +117,31 @@ function updateNotification(
116117
return didChange ? next : previous
117118
}
118119

120+
export function formatAgentStandbySystemNotification(
121+
notification: Pick<
122+
AgentStandbyNotification,
123+
'title' | 'workspaceName' | 'taskTitle' | 'spaceName'
124+
>,
125+
labels: {
126+
standby: string
127+
task: string
128+
space: string
129+
},
130+
): { title: string; body: string } {
131+
const summary = notification.workspaceName
132+
? `${labels.standby} · ${notification.workspaceName}`
133+
: labels.standby
134+
const contextLines = [
135+
notification.taskTitle ? `${labels.task}: ${notification.taskTitle}` : null,
136+
notification.spaceName ? `${labels.space}: ${notification.spaceName}` : null,
137+
].filter((line): line is string => !!line)
138+
139+
return {
140+
title: notification.title,
141+
body: [summary, ...contextLines].join('\n'),
142+
}
143+
}
144+
119145
export function useAgentStandbyNotifications({
120146
maxVisible = 5,
121147
}: {
@@ -124,11 +150,15 @@ export function useAgentStandbyNotifications({
124150
notifications: AgentStandbyNotification[]
125151
dismiss: (id: string) => void
126152
} {
153+
const { t } = useTranslation()
127154
const platform =
128155
typeof window !== 'undefined' && window.opencoveApi?.meta?.platform
129156
? window.opencoveApi.meta.platform
130157
: undefined
131158
const workspaces = useAppStore(state => state.workspaces)
159+
const areSystemNotificationsEnabled = useAppStore(
160+
state => state.agentSettings.systemNotificationsEnabled,
161+
)
132162
const isStandbyBannerEnabled = useAppStore(state => state.agentSettings.standbyBannerEnabled)
133163
const showBranch = useAppStore(state => state.agentSettings.standbyBannerShowBranch)
134164
const showPullRequest = useAppStore(state => state.agentSettings.standbyBannerShowPullRequest)
@@ -243,7 +273,7 @@ export function useAgentStandbyNotifications({
243273

244274
const handleAgentEnteredStandby = useCallback(
245275
(payload: AgentStandbyNotificationPayload) => {
246-
if (!isStandbyBannerEnabled) {
276+
if (!isStandbyBannerEnabled && !areSystemNotificationsEnabled) {
247277
return
248278
}
249279

@@ -276,6 +306,21 @@ export function useAgentStandbyNotifications({
276306
createdAt: Date.now(),
277307
}
278308

309+
if (areSystemNotificationsEnabled && window.opencoveApi?.meta?.isTest !== true) {
310+
const nativeNotification = formatAgentStandbySystemNotification(next, {
311+
standby: t('agentRuntime.standby'),
312+
task: t('settingsPanel.nav.tasks'),
313+
space: t('commandCenter.sections.spaces'),
314+
})
315+
void window.opencoveApi?.system
316+
?.showNotification(nativeNotification)
317+
.catch(() => undefined)
318+
}
319+
320+
if (!isStandbyBannerEnabled) {
321+
return previous
322+
}
323+
279324
if (!shouldResolveBranch && !shouldResolvePullRequest) {
280325
const updated = [next, ...previous]
281326
return updated.length > maxVisible ? updated.slice(0, maxVisible) : updated
@@ -286,10 +331,12 @@ export function useAgentStandbyNotifications({
286331
})
287332
},
288333
[
334+
areSystemNotificationsEnabled,
289335
isStandbyBannerEnabled,
290336
maxVisible,
291337
shouldResolveBranch,
292338
shouldResolvePullRequest,
339+
t,
293340
workspacesById,
294341
],
295342
)

src/contexts/settings/domain/agentSettings.defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export const DEFAULT_AGENT_SETTINGS: AgentSettings = {
4242
focusNodeOnClick: true,
4343
focusNodeTargetZoom: 1,
4444
focusNodeUseVisibleCanvasCenter: true,
45+
systemNotificationsEnabled: true,
4546
standbyBannerEnabled: true,
4647
standbyBannerShowTask: true,
4748
standbyBannerShowSpace: true,

0 commit comments

Comments
 (0)