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 Logger from '../common/logger' ;
8+ import { ITelemetry } from '../common/telemetry' ;
9+ import { getNonce , IRequestMessage , WebviewBase } from '../common/webview' ;
10+ import { ChatSessionWithPR } from './copilotApi' ;
11+ import { CopilotRemoteAgentManager } from './copilotRemoteAgent' ;
12+ import { FolderRepositoryManager } from './folderRepositoryManager' ;
13+ import { IssueModel } from './issueModel' ;
14+ import { RepositoriesManager } from './repositoriesManager' ;
15+
16+ export interface DashboardData {
17+ activeSessions : SessionData [ ] ;
18+ milestoneIssues : IssueData [ ] ;
19+ }
20+
21+ export interface SessionData {
22+ id : string ;
23+ title : string ;
24+ status : string ;
25+ dateCreated : string ;
26+ pullRequest ?: {
27+ number : number ;
28+ title : string ;
29+ url : string ;
30+ } ;
31+ }
32+
33+ export interface IssueData {
34+ number : number ;
35+ title : string ;
36+ assignee ?: string ;
37+ milestone ?: string ;
38+ state : string ;
39+ url : string ;
40+ createdAt : string ;
41+ updatedAt : string ;
42+ }
43+
44+ export class DashboardWebviewProvider extends WebviewBase {
45+ public static readonly viewType = 'github.dashboard' ;
46+ private static readonly ID = 'DashboardWebviewProvider' ;
47+ public static currentPanel ?: DashboardWebviewProvider ;
48+
49+ protected readonly _panel : vscode . WebviewPanel ;
50+
51+ constructor (
52+ private readonly _context : vscode . ExtensionContext ,
53+ private readonly _repositoriesManager : RepositoriesManager ,
54+ private readonly _copilotRemoteAgentManager : CopilotRemoteAgentManager ,
55+ private readonly _telemetry : ITelemetry ,
56+ extensionUri : vscode . Uri ,
57+ panel : vscode . WebviewPanel
58+ ) {
59+ super ( ) ;
60+ this . _panel = panel ;
61+ this . _webview = panel . webview ;
62+ super . initialize ( ) ;
63+
64+ // Set webview options
65+ this . _webview . options = {
66+ enableScripts : true ,
67+ localResourceRoots : [ extensionUri ]
68+ } ;
69+
70+ // Set webview HTML
71+ this . _webview . html = this . getHtmlForWebview ( ) ;
72+
73+ // Listen for panel disposal
74+ this . _register ( this . _panel . onDidDispose ( ( ) => {
75+ DashboardWebviewProvider . currentPanel = undefined ;
76+ } ) ) ;
77+
78+ // Send initial data
79+ this . updateDashboard ( ) ;
80+ }
81+
82+ public static async createOrShow (
83+ context : vscode . ExtensionContext ,
84+ reposManager : RepositoriesManager ,
85+ copilotRemoteAgentManager : CopilotRemoteAgentManager ,
86+ telemetry : ITelemetry ,
87+ extensionUri : vscode . Uri
88+ ) : Promise < void > {
89+ const column = vscode . window . activeTextEditor ?. viewColumn || vscode . ViewColumn . One ;
90+
91+ // If we already have a panel, show it
92+ if ( DashboardWebviewProvider . currentPanel ) {
93+ DashboardWebviewProvider . currentPanel . _panel . reveal ( column ) ;
94+ return ;
95+ }
96+
97+ // Create a new panel
98+ const panel = vscode . window . createWebviewPanel (
99+ DashboardWebviewProvider . viewType ,
100+ 'My Tasks' ,
101+ column ,
102+ {
103+ enableScripts : true ,
104+ retainContextWhenHidden : true ,
105+ localResourceRoots : [ extensionUri ]
106+ }
107+ ) ;
108+
109+ // Set the icon
110+ panel . iconPath = {
111+ light : vscode . Uri . joinPath ( extensionUri , 'resources' , 'icons' , 'github_logo.png' ) ,
112+ dark : vscode . Uri . joinPath ( extensionUri , 'resources' , 'icons' , 'github_logo.png' )
113+ } ;
114+
115+ DashboardWebviewProvider . currentPanel = new DashboardWebviewProvider (
116+ context ,
117+ reposManager ,
118+ copilotRemoteAgentManager ,
119+ telemetry ,
120+ extensionUri ,
121+ panel
122+ ) ;
123+ }
124+
125+ public static refresh ( ) : void {
126+ if ( DashboardWebviewProvider . currentPanel ) {
127+ DashboardWebviewProvider . currentPanel . updateDashboard ( ) ;
128+ }
129+ }
130+
131+ private async updateDashboard ( ) : Promise < void > {
132+ try {
133+ const data = await this . getDashboardData ( ) ;
134+ this . _postMessage ( {
135+ command : 'update-dashboard' ,
136+ data : data
137+ } ) ;
138+ } catch ( error ) {
139+ Logger . error ( `Failed to update dashboard: ${ error } ` , DashboardWebviewProvider . ID ) ;
140+ }
141+ }
142+
143+ private async getDashboardData ( ) : Promise < DashboardData > {
144+ const [ activeSessions , milestoneIssues ] = await Promise . all ( [
145+ this . getActiveSessions ( ) ,
146+ this . getMilestoneIssues ( )
147+ ] ) ;
148+
149+ return {
150+ activeSessions,
151+ milestoneIssues
152+ } ;
153+ }
154+
155+ private async getActiveSessions ( ) : Promise < SessionData [ ] > {
156+ try {
157+ // Create a cancellation token for the request
158+ const source = new vscode . CancellationTokenSource ( ) ;
159+ const token = source . token ;
160+
161+ const sessions = await this . _copilotRemoteAgentManager . provideChatSessions ( token ) ;
162+ return sessions . map ( session => this . convertSessionToData ( session ) ) ;
163+ } catch ( error ) {
164+ Logger . error ( `Failed to get active sessions: ${ error } ` , DashboardWebviewProvider . ID ) ;
165+ return [ ] ;
166+ }
167+ }
168+
169+ private convertSessionToData ( session : ChatSessionWithPR ) : SessionData {
170+ return {
171+ id : session . id ,
172+ title : session . label ,
173+ status : session . status ? session . status . toString ( ) : 'Unknown' ,
174+ dateCreated : session . timing ?. startTime ? new Date ( session . timing . startTime ) . toISOString ( ) : '' ,
175+ pullRequest : session . pullRequest ? {
176+ number : session . pullRequest . number ,
177+ title : session . pullRequest . title ,
178+ url : session . pullRequest . html_url
179+ } : undefined
180+ } ;
181+ }
182+
183+ private async getMilestoneIssues ( ) : Promise < IssueData [ ] > {
184+ try {
185+ const issues : IssueData [ ] = [ ] ;
186+
187+ for ( const folderManager of this . _repositoriesManager . folderManagers ) {
188+ const milestoneIssues = await this . getIssuesForMilestone ( folderManager , 'September 2025' ) ;
189+ issues . push ( ...milestoneIssues ) ;
190+ }
191+
192+ return issues ;
193+ } catch ( error ) {
194+ Logger . error ( `Failed to get milestone issues: ${ error } ` , DashboardWebviewProvider . ID ) ;
195+ return [ ] ;
196+ }
197+ }
198+
199+ private async getIssuesForMilestone ( folderManager : FolderRepositoryManager , milestoneTitle : string ) : Promise < IssueData [ ] > {
200+ try {
201+ // Build query for open issues in the specific milestone
202+ const query = `is:open milestone:"${ milestoneTitle } " assignee:@me` ;
203+ const searchResult = await folderManager . getIssues ( query ) ;
204+
205+ if ( ! searchResult || ! searchResult . items ) {
206+ return [ ] ;
207+ }
208+
209+ return searchResult . items . map ( issue => this . convertIssueToData ( issue ) ) ;
210+ } catch ( error ) {
211+ Logger . debug ( `Failed to get issues for milestone ${ milestoneTitle } : ${ error } ` , DashboardWebviewProvider . ID ) ;
212+ return [ ] ;
213+ }
214+ }
215+
216+ private convertIssueToData ( issue : IssueModel ) : IssueData {
217+ return {
218+ number : issue . number ,
219+ title : issue . title ,
220+ assignee : issue . assignees ?. [ 0 ] ?. login ,
221+ milestone : issue . milestone ?. title ,
222+ state : issue . state ,
223+ url : issue . html_url ,
224+ createdAt : issue . createdAt ,
225+ updatedAt : issue . updatedAt
226+ } ;
227+ }
228+
229+ protected override async _onDidReceiveMessage ( message : IRequestMessage < any > ) : Promise < void > {
230+ switch ( message . command ) {
231+ case 'refresh-dashboard' :
232+ await this . updateDashboard ( ) ;
233+ break ;
234+ case 'open-chat' :
235+ await this . openChatWithQuery ( message . args ?. query ) ;
236+ break ;
237+ case 'open-session' :
238+ await this . openSession ( message . args ?. sessionId ) ;
239+ break ;
240+ case 'open-issue' :
241+ await this . openIssue ( message . args ?. issueUrl ) ;
242+ break ;
243+ case 'open-pull-request' :
244+ await this . openPullRequest ( message . args ?. pullRequest ) ;
245+ break ;
246+ default :
247+ await super . _onDidReceiveMessage ( message ) ;
248+ break ;
249+ }
250+ }
251+
252+ private async openChatWithQuery ( query : string ) : Promise < void > {
253+ if ( ! query ) {
254+ return ;
255+ }
256+
257+ try {
258+ await vscode . commands . executeCommand ( 'workbench.action.chat.open' , { query } ) ;
259+ } catch ( error ) {
260+ Logger . error ( `Failed to open chat with query: ${ error } ` , DashboardWebviewProvider . ID ) ;
261+ vscode . window . showErrorMessage ( 'Failed to open chat. Make sure the Chat extension is available.' ) ;
262+ }
263+ }
264+
265+ private async openSession ( sessionId : string ) : Promise < void > {
266+ if ( ! sessionId ) {
267+ return ;
268+ }
269+
270+ try {
271+ // Open the chat session
272+ await vscode . window . showChatSession ( 'copilot-swe-agent' , sessionId , { } ) ;
273+ } catch ( error ) {
274+ Logger . error ( `Failed to open session: ${ error } ` , DashboardWebviewProvider . ID ) ;
275+ vscode . window . showErrorMessage ( 'Failed to open session.' ) ;
276+ }
277+ }
278+
279+ private async openIssue ( issueUrl : string ) : Promise < void > {
280+ if ( ! issueUrl ) {
281+ return ;
282+ }
283+
284+ try {
285+ await vscode . env . openExternal ( vscode . Uri . parse ( issueUrl ) ) ;
286+ } catch ( error ) {
287+ Logger . error ( `Failed to open issue: ${ error } ` , DashboardWebviewProvider . ID ) ;
288+ vscode . window . showErrorMessage ( 'Failed to open issue.' ) ;
289+ }
290+ }
291+
292+ private async openPullRequest ( pullRequest : { number : number ; title : string ; url : string } ) : Promise < void > {
293+ if ( ! pullRequest ) {
294+ return ;
295+ }
296+
297+ try {
298+ // Try to find the pull request in the current repositories
299+ for ( const folderManager of this . _repositoriesManager . folderManagers ) {
300+ // Parse the URL to get owner and repo
301+ const urlMatch = pullRequest . url . match ( / g i t h u b \. c o m \/ ( [ ^ \/ ] + ) \/ ( [ ^ \/ ] + ) \/ p u l l \/ ( \d + ) / ) ;
302+ if ( urlMatch ) {
303+ const [ , owner , repo ] = urlMatch ;
304+ const pullRequestModel = await folderManager . resolvePullRequest ( owner , repo , pullRequest . number ) ;
305+ if ( pullRequestModel ) {
306+ // Use the extension's command to open the pull request
307+ await vscode . commands . executeCommand ( 'pr.openDescription' , pullRequestModel ) ;
308+ return ;
309+ }
310+ }
311+ }
312+
313+ // Fallback to opening externally if we can't find the PR locally
314+ await vscode . env . openExternal ( vscode . Uri . parse ( pullRequest . url ) ) ;
315+ } catch ( error ) {
316+ Logger . error ( `Failed to open pull request: ${ error } ` , DashboardWebviewProvider . ID ) ;
317+ // Fallback to opening externally
318+ try {
319+ await vscode . env . openExternal ( vscode . Uri . parse ( pullRequest . url ) ) ;
320+ } catch ( fallbackError ) {
321+ vscode . window . showErrorMessage ( 'Failed to open pull request.' ) ;
322+ }
323+ }
324+ }
325+
326+ private getHtmlForWebview ( ) : string {
327+ const nonce = getNonce ( ) ;
328+ const uri = vscode . Uri . joinPath ( this . _context . extensionUri , 'dist' , 'webview-dashboard.js' ) ;
329+
330+ return `<!DOCTYPE html>
331+ <html lang="en">
332+ <head>
333+ <meta charset="UTF-8">
334+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https:; script-src 'nonce-${ nonce } '; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
335+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
336+ <title>GitHub Dashboard</title>
337+ </head>
338+ <body>
339+ <div id="app"></div>
340+ <script nonce="${ nonce } " src="${ this . _webview ! . asWebviewUri ( uri ) . toString ( ) } "></script>
341+ </body>
342+ </html>` ;
343+ }
344+ }
0 commit comments