Skip to content

Commit 9733a92

Browse files
authored
Add initial session log view (#7064)
* Initial version of session log viewer webview * Hook up to session view button * Add markdown it dep
1 parent 9f33be1 commit 9733a92

File tree

18 files changed

+920
-25
lines changed

18 files changed

+920
-25
lines changed

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1651,6 +1651,11 @@
16511651
"title": "%command.notifications.configureNotificationsViewlet.title%",
16521652
"category": "%command.notifications.category%",
16531653
"icon": "$(gear)"
1654+
},
1655+
{
1656+
"command": "padawan.openSessionLog",
1657+
"title": "Open Padawan Session Log",
1658+
"category": "%command.pull.request.category%"
16541659
}
16551660
],
16561661
"viewsWelcome": [
@@ -3993,7 +3998,9 @@
39933998
"fast-deep-equal": "^3.1.3",
39943999
"jszip": "^3.10.1",
39954000
"lru-cache": "6.0.0",
4001+
"markdown-it": "^14.1.0",
39964002
"marked": "^4.0.10",
4003+
"monaco-editor": "^0.52.2",
39974004
"react": "^16.12.0",
39984005
"react-dom": "^16.12.0",
39994006
"ssh-config": "4.1.1",
@@ -4005,4 +4012,4 @@
40054012
"vsls": "^0.3.967"
40064013
},
40074014
"license": "MIT"
4008-
}
4015+
}

src/common/timelineEvent.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,19 @@ export interface ReopenedEvent {
151151
createdAt: string;
152152
}
153153

154+
export interface SessionLinkInfo {
155+
host: string;
156+
owner: string;
157+
repo: string;
158+
pullId: number;
159+
}
160+
154161
export interface CopilotStartedEvent {
155162
id: string;
156163
event: EventType.CopilotStarted;
157164
createdAt: string;
158165
onBehalfOf: IAccount;
159-
sessionUrl?: string;
166+
sessionLink?: SessionLinkInfo;
160167
}
161168

162169
export interface CopilotFinishedEvent {
@@ -171,7 +178,7 @@ export interface CopilotFinishedErrorEvent {
171178
event: EventType.CopilotFinishedError;
172179
createdAt: string;
173180
onBehalfOf: IAccount;
174-
sessionUrl: string;
181+
sessionLink: SessionLinkInfo;
175182
}
176183

177184
export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent;

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { PRNotificationDecorationProvider } from './view/prNotificationDecoratio
4444
import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';
4545
import { ReviewManager, ShowPullRequest } from './view/reviewManager';
4646
import { ReviewsManager } from './view/reviewsManager';
47+
import { SessionLogViewManager } from './view/sessionLogView';
4748
import { TreeDecorationProviders } from './view/treeDecorationProviders';
4849
import { WebviewViewCoordinator } from './view/webviewViewCoordinator';
4950

@@ -246,6 +247,9 @@ async function init(
246247

247248
context.subscriptions.push(new GitLensIntegration());
248249

250+
const sessionLogViewManager = new SessionLogViewManager(credentialStore, context);
251+
context.subscriptions.push(sessionLogViewManager);
252+
249253
await vscode.commands.executeCommand('setContext', 'github:initialized', true);
250254

251255
registerPostCommitCommandsProvider(reposManager, git);

src/github/copilotApi.ts

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,24 +112,39 @@ export class CopilotApi {
112112
return copilotSteps;
113113
}
114114

115-
public async getAllSessions(pullRequest: PullRequestModel): Promise<{
116-
id: string;
117-
state: string;
118-
last_updated_at: string;
119-
}[]> {
120-
const response = await fetch(`https://api.githubcopilot.com/agents/sessions/resource/pull/${pullRequest.id}`, {
121-
headers: {
122-
Authorization: `Bearer ${this.token}`,
123-
Accept: 'application/json',
124-
},
125-
});
115+
public async getAllSessions(pullRequest: PullRequestModel | undefined): Promise<SessionInfo[]> {
116+
const response = await fetch(
117+
pullRequest
118+
? `https://api.githubcopilot.com/agents/sessions/resource/pull/${pullRequest.id}`
119+
: 'https://api.githubcopilot.com/agents/sessions',
120+
{
121+
headers: {
122+
Authorization: `Bearer ${this.token}`,
123+
Accept: 'application/json',
124+
},
125+
});
126126
if (!response.ok) {
127127
throw new Error(`Failed to fetch sessions: ${response.statusText}`);
128128
}
129129
const sessions = await response.json();
130130
return sessions.sessions;
131131
}
132132

133+
public async getSessionInfo(sessionId: string): Promise<SessionInfo> {
134+
const response = await fetch(`https://api.githubcopilot.com/agents/sessions/${sessionId}`, {
135+
method: 'GET',
136+
headers: {
137+
Authorization: `Bearer ${this.token}`,
138+
'Accept': 'application/json'
139+
}
140+
});
141+
if (!response.ok) {
142+
throw new Error(`Failed to fetch session: ${response.statusText}`);
143+
}
144+
145+
return (await response.json()) as SessionInfo;
146+
}
147+
133148
public async getLogsFromSession(sessionId: string): Promise<string> {
134149
const logsResponse = await fetch(`https://api.githubcopilot.com/agents/sessions/${sessionId}/logs`, {
135150
method: 'GET',
@@ -143,4 +158,26 @@ export class CopilotApi {
143158
}
144159
return await logsResponse.text();
145160
}
146-
}
161+
}
162+
163+
164+
export interface SessionInfo {
165+
id: string;
166+
name: string;
167+
user_id: number;
168+
agent_id: number;
169+
logs: string;
170+
logs_blob_id: string;
171+
state: string;
172+
owner_id: number;
173+
repo_id: number;
174+
resource_type: string;
175+
resource_id: number;
176+
last_updated_at: string;
177+
created_at: string;
178+
completed_at: string;
179+
event_type: string;
180+
workflow_run_id: number;
181+
premium_requests: number;
182+
error: string | null;
183+
}

src/github/pullRequestOverview.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ import { disposeAll } from '../common/lifecycle';
1212
import Logger from '../common/logger';
1313
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
1414
import { ITelemetry } from '../common/telemetry';
15-
import { ReviewEvent } from '../common/timelineEvent';
15+
import { ReviewEvent, SessionLinkInfo } from '../common/timelineEvent';
1616
import { asPromise, formatError } from '../common/utils';
1717
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
18+
import { SessionLogViewManager } from '../view/sessionLogView';
1819
import { FolderRepositoryManager } from './folderRepositoryManager';
1920
import {
2021
GithubItemStateEnum,
@@ -356,6 +357,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
356357
return this.reRequestReview(message);
357358
case 'pr.revert':
358359
return this.revert(message);
360+
case 'pr.open-session-log':
361+
return this.openSessionLog(message.args.link);
359362
}
360363
}
361364

@@ -451,6 +454,14 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
451454
}
452455
}
453456

457+
private async openSessionLog(_message: IRequestMessage<{ link: SessionLinkInfo }>): Promise<void> {
458+
try {
459+
SessionLogViewManager.instance?.openForPull(this._item);
460+
} catch (e) {
461+
Logger.error(`Open session log view failed: ${formatError(e)}`, PullRequestOverviewPanel.ID);
462+
}
463+
}
464+
454465
private async openChanges(): Promise<void> {
455466
return PullRequestModel.openChanges(this._folderRepositoryManager, this._item);
456467
}

src/github/utils.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1002,7 +1002,12 @@ export function parseSelectRestTimelineEvents(
10021002
events: OctokitCommon.ListEventsForTimelineResponse[]
10031003
): Common.TimelineEvent[] {
10041004
const parsedEvents: Common.TimelineEvent[] = [];
1005-
const sessionUrl = `https://${issueModel.githubRepository.remote.gitProtocol.host}/${issueModel.githubRepository.remote.owner}/${issueModel.githubRepository.remote.repositoryName}/pull/${issueModel.number}/agent-sessions`;
1005+
const sessionLink: Common.SessionLinkInfo = {
1006+
host: issueModel.githubRepository.remote.gitProtocol.host,
1007+
owner: issueModel.githubRepository.remote.owner,
1008+
repo: issueModel.githubRepository.remote.repositoryName,
1009+
pullId: issueModel.number
1010+
};
10061011
let indexLastStart = -1;
10071012
for (const event of events) {
10081013
const eventNode = event as { created_at?: string; node_id?: string; actor: RestAccount };
@@ -1028,14 +1033,14 @@ export function parseSelectRestTimelineEvents(
10281033
event: Common.EventType.CopilotFinishedError,
10291034
createdAt: eventNode.created_at,
10301035
onBehalfOf: parseAccount(eventNode.actor),
1031-
sessionUrl
1036+
sessionLink
10321037
});
10331038
}
10341039
}
10351040
}
10361041
if (indexLastStart > -1) {
10371042
const startEvent: Common.CopilotStartedEvent = parsedEvents[indexLastStart] as Common.CopilotStartedEvent;
1038-
startEvent.sessionUrl = sessionUrl;
1043+
startEvent.sessionLink = sessionLink;
10391044
}
10401045
return parsedEvents;
10411046
}

src/view/sessionLogView.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import type * as messages from '../../webviews/sessionLogView/messages';
8+
import { AuthProvider } from '../common/authentication';
9+
import { Disposable } from '../common/lifecycle';
10+
import { CopilotApi } from '../github/copilotApi';
11+
import { CredentialStore } from '../github/credentials';
12+
import { PullRequestModel } from '../github/pullRequestModel';
13+
import { hasEnterpriseUri } from '../github/utils';
14+
15+
export class SessionLogViewManager extends Disposable {
16+
static instance: SessionLogViewManager | undefined;
17+
18+
constructor(
19+
private readonly credentialStore: CredentialStore,
20+
private readonly context: vscode.ExtensionContext,
21+
) {
22+
super();
23+
24+
SessionLogViewManager.instance = this;
25+
26+
this._register(vscode.commands.registerCommand('padawan.openSessionLog', async () => {
27+
const copilotApi = await getCopilotApi(credentialStore);
28+
if (!copilotApi) {
29+
vscode.window.showErrorMessage(vscode.l10n.t('You must be authenticated to view sessions.'));
30+
return;
31+
}
32+
33+
const allSessions = await copilotApi.getAllSessions(undefined);
34+
if (!allSessions.length) {
35+
vscode.window.showErrorMessage(vscode.l10n.t('No sessions found.'));
36+
return;
37+
}
38+
39+
const sessionItems = allSessions.map(session => ({
40+
label: session.name || session.id,
41+
description: session.created_at ? new Date(session.created_at).toLocaleString() : undefined,
42+
detail: session.id,
43+
sessionId: session.id
44+
}));
45+
46+
const picked = await vscode.window.showQuickPick(sessionItems, {
47+
placeHolder: vscode.l10n.t('Select a session log to view')
48+
});
49+
50+
if (!picked) {
51+
return;
52+
}
53+
54+
return this.open(picked.sessionId);
55+
}));
56+
}
57+
58+
async openForPull(pullRequest: PullRequestModel): Promise<void> {
59+
const copilotApi = await getCopilotApi(this.credentialStore);
60+
if (!copilotApi) {
61+
return;
62+
}
63+
64+
const sessionId = (await copilotApi.getAllSessions(pullRequest))[0].id;
65+
if (!sessionId) {
66+
vscode.window.showErrorMessage(vscode.l10n.t('No sessions found for this pull request.'));
67+
return;
68+
}
69+
70+
return this.open(sessionId);
71+
}
72+
73+
async open(sessionId: string): Promise<void> {
74+
const copilotApi = await getCopilotApi(this.credentialStore);
75+
if (!copilotApi) {
76+
return;
77+
}
78+
79+
const webviewPanel = vscode.window.createWebviewPanel('padawanSessionView', vscode.l10n.t('Session Logs'), vscode.ViewColumn.Active);
80+
81+
const distDir = vscode.Uri.joinPath(this.context.extensionUri, 'dist');
82+
83+
webviewPanel.webview.options = {
84+
enableScripts: true,
85+
localResourceRoots: [
86+
distDir
87+
]
88+
};
89+
webviewPanel.webview.html = `<!DOCTYPE html>
90+
<html lang="en">
91+
<head>
92+
<meta charset="UTF-8">
93+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
94+
<title>Session Log</title>
95+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline' ${webviewPanel.webview.cspSource}; script-src ${webviewPanel.webview.cspSource};">
96+
</head>
97+
<body>
98+
<div id="app"></div>
99+
100+
<script type="module" src="${webviewPanel.webview.asWebviewUri(vscode.Uri.joinPath(distDir, 'webview-session-log-view.js'))}"></script>
101+
</body>
102+
</html>`;
103+
104+
const [info, logs] = await Promise.all([
105+
copilotApi.getSessionInfo(sessionId),
106+
copilotApi.getLogsFromSession(sessionId)
107+
]);
108+
109+
webviewPanel.webview.postMessage({
110+
type: 'init',
111+
info,
112+
logs,
113+
} as messages.InitMessage);
114+
}
115+
}
116+
117+
async function getCopilotApi(credentialStore: CredentialStore): Promise<CopilotApi | undefined> {
118+
let authProvider: AuthProvider | undefined;
119+
if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) {
120+
authProvider = AuthProvider.githubEnterprise;
121+
} else if (credentialStore.isAuthenticated(AuthProvider.github)) {
122+
authProvider = AuthProvider.github;
123+
} else {
124+
return;
125+
}
126+
127+
const github = credentialStore.getHub(authProvider);
128+
if (!github || !github.octokit) {
129+
return;
130+
}
131+
132+
const { token } = await github.octokit.api.auth() as { token: string };
133+
return new CopilotApi(github.octokit, token);
134+
}

webpack.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ async function getWebviewConfig(mode, env, entry) {
134134
test: /\.svg/,
135135
use: ['svg-inline-loader'],
136136
},
137+
{
138+
test: /\.ttf$/,
139+
type: 'asset/resource'
140+
},
137141
],
138142
},
139143
resolve: {
@@ -378,6 +382,7 @@ module.exports =
378382
'webview-pr-description': './webviews/editorWebview/index.ts',
379383
'webview-open-pr-view': './webviews/activityBarView/index.ts',
380384
'webview-create-pr-view-new': './webviews/createPullRequestViewNew/index.ts',
385+
'webview-session-log-view': './webviews/sessionLogView/index.tsx',
381386
}),
382387
]);
383388
};

webviews/common/context.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { createContext } from 'react';
77
import { CloseResult } from '../../common/views';
88
import { IComment } from '../../src/common/comment';
9-
import { EventType, ReviewEvent, TimelineEvent } from '../../src/common/timelineEvent';
9+
import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../../src/common/timelineEvent';
1010
import { IProjectItem, MergeMethod, ReadyForReview } from '../../src/github/interface';
1111
import { ChangeAssigneesReply, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, SubmitReviewReply } from '../../src/github/views';
1212
import { getState, setState, updateState } from './cache';
@@ -256,6 +256,8 @@ export class PRContext {
256256
});
257257
};
258258

259+
public openSessionLog = (link: SessionLinkInfo) => this.postMessage({ command: 'pr.open-session-log', args: { link } });
260+
259261
setPR = (pr: PullRequest) => {
260262
this.pr = pr;
261263
setState(this.pr);

0 commit comments

Comments
 (0)