Skip to content

Commit 4e3041e

Browse files
authored
feat(notifications): support copilot agent session (#3384)
* feat(notifications): support copilot agent session Signed-off-by: Adam Setch <adam.setch@outlook.com> * feat(notifications): support copilot agent session Signed-off-by: Adam Setch <adam.setch@outlook.com> * fix(sonar): build agent session url with URL builder (typescript:S8786) --------- Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent c443059 commit 4e3041e

28 files changed

Lines changed: 636 additions & 7 deletions

‎src/renderer/__mocks__/notifications-mocks.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { GitPullRequestIcon, IssueOpenedIcon, TagIcon } from '@primer/octicons-react';
1+
import {
2+
AgentIcon,
3+
CopilotIcon,
4+
GitPullRequestIcon,
5+
IssueOpenedIcon,
6+
TagIcon,
7+
} from '@primer/octicons-react';
28

39
import {
410
type AccountNotifications,
@@ -217,6 +223,43 @@ export const mockGithubEnterpriseGitifyNotifications: GitifyNotification[] = [
217223

218224
export const mockGitifyNotification: GitifyNotification = mockGitHubCloudGitifyNotifications[0];
219225

226+
/**
227+
* Mock Gitify Notification for a GitHub Copilot cloud agent session.
228+
*
229+
* Subject type `AgentSessionThread`, reason `agent_session_finished`. The actor
230+
* is Copilot, surfaced through the handler's actor icon and `Bot` user type.
231+
*/
232+
export const mockAgentSessionGitifyNotification: GitifyNotification = {
233+
account: mockGitHubCloudAccount,
234+
order: 0,
235+
id: '26096167998',
236+
unread: true,
237+
reason: {
238+
code: 'agent_session_finished',
239+
title: 'Agent Session Finished',
240+
description: 'A GitHub Copilot cloud agent session finished running.',
241+
},
242+
updatedAt: '2026-10-05T02:34:59Z',
243+
subject: {
244+
title: 'Analyzing and fixing GitHub Actions job failure',
245+
url: null,
246+
latestCommentUrl: null,
247+
type: 'AgentSessionThread',
248+
},
249+
repository: mockGitHubRepository,
250+
display: {
251+
number: '',
252+
title: 'Analyzing and fixing GitHub Actions job failure',
253+
type: 'Agent Session Thread',
254+
icon: {
255+
type: AgentIcon,
256+
color: IconColor.GRAY,
257+
},
258+
defaultUserType: 'Bot',
259+
defaultUserIcon: CopilotIcon,
260+
},
261+
};
262+
220263
/** Same shape as cloud notification, but bound to a Gitea account. */
221264
export const mockGiteaGitifyNotification: GitifyNotification = {
222265
...mockGitifyNotification,

‎src/renderer/components/avatars/AvatarWithFallback.test.tsx‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { fireEvent, screen } from '@testing-library/react';
22

3+
import { CopilotIcon } from '@primer/octicons-react';
4+
35
import { renderWithProviders } from '../../__helpers__/test-utils';
46

57
import { type Link, Size } from '../../types';
@@ -39,6 +41,14 @@ describe('renderer/components/avatars/AvatarWithFallback.tsx', () => {
3941
expect(tree.container).toMatchSnapshot();
4042
});
4143

44+
it('renders a provided fallback icon override', () => {
45+
const tree = renderWithProviders(
46+
<AvatarWithFallback {...props} icon={CopilotIcon} src={undefined} />,
47+
);
48+
49+
expect(tree.container).toMatchSnapshot();
50+
});
51+
4252
it('renders the fallback icon when the image fails to load (isBroken = true) - human user', () => {
4353
renderWithProviders(<AvatarWithFallback {...props} />);
4454

‎src/renderer/components/avatars/AvatarWithFallback.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type React from 'react';
2+
import type { FC } from 'react';
23
import { createElement, useState } from 'react';
34

5+
import type { OcticonProps } from '@primer/octicons-react';
46
import { Avatar, Stack, Truncate } from '@primer/react';
57

68
import { type Link, Size, type UserType } from '../../types';
@@ -14,6 +16,8 @@ export interface AvatarWithFallbackProps {
1416
name?: string;
1517
size?: number;
1618
userType?: UserType;
19+
/** Optional fallback icon overriding the user-type default. */
20+
icon?: FC<OcticonProps>;
1721
}
1822

1923
export const AvatarWithFallback: React.FC<AvatarWithFallbackProps> = ({
@@ -22,11 +26,12 @@ export const AvatarWithFallback: React.FC<AvatarWithFallbackProps> = ({
2226
name,
2327
size = Size.MEDIUM,
2428
userType = 'User',
29+
icon,
2530
}) => {
2631
const [hasBrokenAvatarSource, setHasBrokenAvatarSource] = useState(false);
2732

2833
const isNonHuman = isNonHumanUser(userType);
29-
const defaultUserIcon = getDefaultUserIcon(userType);
34+
const defaultUserIcon = icon ?? getDefaultUserIcon(userType);
3035

3136
// TODO explore using AnchoredOverlay component (https://primer.style/components/anchored-overlay/react/alpha) to render Avatar Card on hover
3237
return (

‎src/renderer/components/avatars/__snapshots__/AvatarWithFallback.test.tsx.snap‎

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/renderer/components/filters/__snapshots__/ReasonFilter.test.tsx.snap‎

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/renderer/components/filters/__snapshots__/SubjectTypeFilter.test.tsx.snap‎

Lines changed: 29 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/renderer/components/notifications/NotificationFooter.test.tsx‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { screen } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33

44
import { renderWithProviders } from '../../__helpers__/test-utils';
5-
import { mockGitifyNotification } from '../../__mocks__/notifications-mocks';
5+
import {
6+
mockAgentSessionGitifyNotification,
7+
mockGitifyNotification,
8+
} from '../../__mocks__/notifications-mocks';
69

710
import type { GitifyNotificationUser, Link } from '../../types';
811

@@ -63,6 +66,17 @@ describe('renderer/components/notifications/NotificationFooter.tsx', () => {
6366
expect(tree.container).toMatchSnapshot();
6467
});
6568

69+
it('presents the Copilot actor for an agent session notification', () => {
70+
const tree = renderWithProviders(
71+
<NotificationFooter notification={mockAgentSessionGitifyNotification} />,
72+
);
73+
74+
expect(tree.container).toMatchSnapshot();
75+
// Agent sessions have no enriched actor, so no profile button is rendered.
76+
expect(screen.queryByTestId('view-profile')).not.toBeInTheDocument();
77+
expect(screen.getByText('Agent Session Finished')).toBeInTheDocument();
78+
});
79+
6680
it('should open notification user profile', async () => {
6781
const openExternalLinkSpy = vi.spyOn(comms, 'openExternalLink').mockImplementation(vi.fn());
6882

‎src/renderer/components/notifications/NotificationFooter.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ export const NotificationFooter: FC<NotificationFooterProps> = ({
5050
/>
5151
</button>
5252
) : (
53-
<AvatarWithFallback size={Size.SMALL} userType={notification.display.defaultUserType} />
53+
<AvatarWithFallback
54+
icon={notification.display.defaultUserIcon}
55+
size={Size.SMALL}
56+
userType={notification.display.defaultUserType}
57+
/>
5458
)}
5559

5660
<Stack direction="horizontal" gap="none">

‎src/renderer/components/notifications/__snapshots__/NotificationFooter.test.tsx.snap‎

Lines changed: 75 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)