Skip to content

Show Copilot start/stop in the timeline #6966

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
May 22, 2025
Merged
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
1 change: 0 additions & 1 deletion .eslintrc.base.json
Original file line number Diff line number Diff line change
@@ -55,7 +55,6 @@
"no-sequences": "error",
"no-template-curly-in-string": "warn",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "warn",
"no-unneeded-ternary": "error",
"no-use-before-define": "off",
"no-useless-call": "error",
1 change: 1 addition & 0 deletions resources/icons/briefcase.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions resources/icons/tasklist.svg

Unable to render rich display

Invalid image source.

21 changes: 19 additions & 2 deletions src/common/timelineEvent.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ export enum EventType {
CrossReferenced,
Closed,
Reopened,
CopilotStarted,
CopilotFinished,
Other,
}

@@ -75,7 +77,7 @@ export interface CommitEvent {
htmlUrl: string;
message: string;
bodyHTML?: string;
authoredDate: Date;
committedDate: Date;
}

export interface NewCommitsSinceReviewEvent {
@@ -148,4 +150,19 @@ export interface ReopenedEvent {
createdAt: string;
}

export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent;
export interface CopilotStartedEvent {
id: string;
event: EventType.CopilotStarted;
createdAt: string;
onBehalfOf: IAccount;
sessionUrl?: string;
}

export interface CopilotFinishedEvent {
id: string;
event: EventType.CopilotFinished;
createdAt: string;
onBehalfOf: IAccount;
}

export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | CopilotStartedEvent | CopilotFinishedEvent;
1 change: 1 addition & 0 deletions src/github/common.ts
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ export namespace OctokitCommon {
export type Commit = CompareCommits['commits'][0];
export type CommitFiles = CompareCommits['files']
export type Notification = Endpoints['GET /notifications']['response']['data'][0];
export type ListEventsForTimelineResponse = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/timeline']['response']['data'][0];
}

export type Schema = { [key: string]: any, definitions: any[]; };
4 changes: 2 additions & 2 deletions src/github/folderRepositoryManager.ts
Original file line number Diff line number Diff line change
@@ -53,8 +53,8 @@ import {
getOverrideBranch,
getPRFetchQuery,
loginComparator,
parseCombinedTimelineEvents,
parseGraphQLPullRequest,
parseGraphQLTimelineEvents,
parseGraphQLUser,
teamComparator,
variableSubstitution,
@@ -1728,7 +1728,7 @@ export class FolderRepositoryManager extends Disposable {
*/
this.telemetry.sendTelemetryEvent('pr.merge.success');
this._onDidMergePullRequest.fire();
return { merged: true, message: '', timeline: await parseGraphQLTimelineEvents(result.data?.mergePullRequest.pullRequest.timelineItems.nodes ?? [], pullRequest.githubRepository) };
return { merged: true, message: '', timeline: await parseCombinedTimelineEvents(result.data?.mergePullRequest.pullRequest.timelineItems.nodes ?? [], await pullRequest.getRestOnlyTimelineEvents(), pullRequest.githubRepository) };
})
.catch(e => {
/* __GDPR__
4 changes: 2 additions & 2 deletions src/github/graphql.ts
Original file line number Diff line number Diff line change
@@ -184,7 +184,7 @@ export interface Commit {
};
oid: string;
message: string;
authoredDate: Date;
committedDate: Date;
};

url: string;
@@ -267,7 +267,7 @@ export interface TimelineEventsResponse {

export interface LatestCommit {
commit: {
authoredDate: string;
committedDate: string;
}
}

37 changes: 32 additions & 5 deletions src/github/issueModel.ts
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { IComment } from '../common/comment';
import { COPILOT_ACCOUNTS, IComment } from '../common/comment';
import Logger from '../common/logger';
import { Remote } from '../common/remote';
import { ClosedEvent, EventType, TimelineEvent } from '../common/timelineEvent';
import { formatError } from '../common/utils';
import { OctokitCommon } from './common';
import { GitHubRepository } from './githubRepository';
import {
AddIssueCommentResponse,
@@ -21,7 +22,7 @@ import {
UpdateIssueResponse,
} from './graphql';
import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue } from './interface';
import { convertRESTIssueToRawPullRequest, parseGraphQlIssueComment, parseGraphQLTimelineEvents } from './utils';
import { convertRESTIssueToRawPullRequest, parseCombinedTimelineEvents, parseGraphQlIssueComment, parseSelectRestTimelineEvents, restPaginate } from './utils';

export class IssueModel<TItem extends Issue = Issue> {
static ID = 'IssueModel';
@@ -325,6 +326,32 @@ export class IssueModel<TItem extends Issue = Issue> {
return this.item.projectItems;
}

/**
* TODO: @alexr00 we should delete this https://github.com/microsoft/vscode-pull-request-github/issues/6965
*/
async getRestOnlyTimelineEvents(): Promise<TimelineEvent[]> {
if (!COPILOT_ACCOUNTS[this.author.login]) {
return [];
}

Logger.debug(`Fetch Copilot timeline events of issue #${this.number} - enter`, IssueModel.ID);

const { octokit, remote } = await this.githubRepository.ensure();
try {
const timeline = await restPaginate<typeof octokit.api.issues.listEventsForTimeline, OctokitCommon.ListEventsForTimelineResponse>(octokit.api.issues.listEventsForTimeline, {
issue_number: this.number,
owner: remote.owner,
repo: remote.repositoryName,
per_page: 100
});

return parseSelectRestTimelineEvents(this, timeline);
} catch (e) {
Logger.error(`Error fetching Copilot timeline events of issue #${this.number} - ${formatError(e)}`, IssueModel.ID);
return [];
}
}

async getIssueTimelineEvents(): Promise<TimelineEvent[]> {
Logger.debug(`Fetch timeline events of issue #${this.number} - enter`, IssueModel.ID);
const githubRepository = this.githubRepository;
@@ -345,7 +372,7 @@ export class IssueModel<TItem extends Issue = Issue> {
return [];
}
const ret = data.repository.pullRequest.timelineItems.nodes;
const events = await parseGraphQLTimelineEvents(ret, githubRepository);
const events = await parseCombinedTimelineEvents(ret, await this.getRestOnlyTimelineEvents(), githubRepository);

return events;
} catch (e) {
@@ -381,8 +408,8 @@ export class IssueModel<TItem extends Issue = Issue> {
...(data.repository.pullRequest.comments.nodes.flatMap(node => node.reactions.nodes.map(reaction => new Date(reaction.createdAt)))),
...(data.repository.pullRequest.timelineItems.nodes.map(node => {
const latestCommit = node as Partial<LatestCommit>;
if (latestCommit.commit?.authoredDate) {
return new Date(latestCommit.commit.authoredDate);
if (latestCommit.commit?.committedDate) {
return new Date(latestCommit.commit.committedDate);
}
const latestReviewThread = node as Partial<LatestReviewThread>;
if ((latestReviewThread.comments?.nodes.length ?? 0) > 0) {
55 changes: 30 additions & 25 deletions src/github/pullRequestModel.ts
Original file line number Diff line number Diff line change
@@ -76,12 +76,12 @@ import {
getReactionGroup,
insertNewCommitsSinceReview,
parseAccount,
parseCombinedTimelineEvents,
parseGraphQLComment,
parseGraphQLReaction,
parseGraphQLReviewers,
parseGraphQLReviewEvent,
parseGraphQLReviewThread,
parseGraphQLTimelineEvents,
parseMergeability,
parseMergeQueueEntry,
RestAccount,
@@ -1162,40 +1162,45 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
* Get the timeline events of a pull request, including comments, reviews, commits, merges, deletes, and assigns.
*/
async getTimelineEvents(): Promise<TimelineEvent[]> {
Logger.debug(`Fetch timeline events of PR #${this.number} - enter`, PullRequestModel.ID);
const { query, remote, schema } = await this.githubRepository.ensure();

try {
const [{ data }, latestReviewCommitInfo, currentUser, reviewThreads] = await Promise.all([
query<TimelineEventsResponse>({
const getTimelineEvents = async () => {
Logger.debug(`Fetch timeline events of PR #${this.number} - enter`, PullRequestModel.ID);
const { query, remote, schema } = await this.githubRepository.ensure();
try {
const { data } = await query<TimelineEventsResponse>({
query: schema.TimelineEvents,
variables: {
owner: remote.owner,
name: remote.repositoryName,
number: this.number,
},
}),
this.getViewerLatestReviewCommit(),
(await this.githubRepository.getAuthenticatedUser()).login,
this.getReviewThreads()
]);
});

if (data.repository === null) {
Logger.error('Unexpected null repository when fetching timeline', PullRequestModel.ID);
if (data.repository === null) {
Logger.error('Unexpected null repository when fetching timeline', PullRequestModel.ID);
}
return data;
} catch (e) {
Logger.error(`Failed to get pull request timeline events: ${e}`, PullRequestModel.ID);
console.log(e);
return undefined;
}
};

const ret = data.repository?.pullRequest.timelineItems.nodes;
const events = ret ? await parseGraphQLTimelineEvents(ret, this.githubRepository) : [];
const [data, latestReviewCommitInfo, currentUser, reviewThreads] = await Promise.all([
getTimelineEvents(),
this.getViewerLatestReviewCommit(),
(await this.githubRepository.getAuthenticatedUser()).login,
this.getReviewThreads()
]);

this.addReviewTimelineEventComments(events, reviewThreads);
insertNewCommitsSinceReview(events, latestReviewCommitInfo?.sha, currentUser, this.head);
Logger.debug(`Fetch timeline events of PR #${this.number} - done`, PullRequestModel.ID);
return events;
} catch (e) {
Logger.error(`Failed to get pull request timeline events: ${e}`, PullRequestModel.ID);
console.log(e);
return [];
}

const ret = data?.repository?.pullRequest.timelineItems.nodes ?? [];
const events = await parseCombinedTimelineEvents(ret, await this.getRestOnlyTimelineEvents(), this.githubRepository);

this.addReviewTimelineEventComments(events, reviewThreads);
insertNewCommitsSinceReview(events, latestReviewCommitInfo?.sha, currentUser, this.head);
Logger.debug(`Fetch timeline events of PR #${this.number} - done`, PullRequestModel.ID);
return events;
}

protected override getUpdatesQuery(schema: any): any {
4 changes: 2 additions & 2 deletions src/github/queriesShared.gql
Original file line number Diff line number Diff line change
@@ -121,7 +121,7 @@ fragment Commit on PullRequestCommit {
}
oid
message
authoredDate
committedDate
}
url
}
@@ -480,7 +480,7 @@ query LatestUpdates($owner: String!, $name: String!, $number: Int!, $since: Date
}
... on PullRequestCommit {
commit {
authoredDate
committedDate
}
}
... on PullRequestReview {
Loading
Oops, something went wrong.