Skip to content

Commit 6e51175

Browse files
committed
complexity
1 parent 77753eb commit 6e51175

File tree

5 files changed

+243
-15
lines changed

5 files changed

+243
-15
lines changed

src/github/dashboardWebviewProvider.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import Logger from '../common/logger';
88
import { ITelemetry } from '../common/telemetry';
99
import { getNonce, IRequestMessage, WebviewBase } from '../common/webview';
10+
import { ComplexityService } from '../issues/complexityService';
1011
import { ChatSessionWithPR } from './copilotApi';
1112
import { CopilotRemoteAgentManager } from './copilotRemoteAgent';
1213
import { FolderRepositoryManager } from './folderRepositoryManager';
@@ -39,6 +40,7 @@ export interface IssueData {
3940
url: string;
4041
createdAt: string;
4142
updatedAt: string;
43+
complexity?: number;
4244
}
4345

4446
export class DashboardWebviewProvider extends WebviewBase {
@@ -47,6 +49,7 @@ export class DashboardWebviewProvider extends WebviewBase {
4749
public static currentPanel?: DashboardWebviewProvider;
4850

4951
protected readonly _panel: vscode.WebviewPanel;
52+
private readonly _complexityService: ComplexityService;
5053

5154
constructor(
5255
private readonly _context: vscode.ExtensionContext,
@@ -59,6 +62,7 @@ export class DashboardWebviewProvider extends WebviewBase {
5962
super();
6063
this._panel = panel;
6164
this._webview = panel.webview;
65+
this._complexityService = new ComplexityService();
6266
super.initialize();
6367

6468
// Set webview options
@@ -206,14 +210,29 @@ export class DashboardWebviewProvider extends WebviewBase {
206210
return [];
207211
}
208212

209-
return searchResult.items.map(issue => this.convertIssueToData(issue));
213+
return Promise.all(searchResult.items.map(issue => this.convertIssueToData(issue)));
210214
} catch (error) {
211215
Logger.debug(`Failed to get issues for milestone ${milestoneTitle}: ${error}`, DashboardWebviewProvider.ID);
212216
return [];
213217
}
214218
}
215219

216-
private convertIssueToData(issue: IssueModel): IssueData {
220+
private async convertIssueToData(issue: IssueModel): Promise<IssueData> {
221+
let complexity: number | undefined;
222+
223+
// Check if complexity is already calculated (from IssueItem)
224+
if ((issue as any).complexity?.score) {
225+
complexity = (issue as any).complexity.score;
226+
} else {
227+
// Calculate complexity on demand
228+
try {
229+
const complexityResult = await this._complexityService.calculateComplexity(issue);
230+
complexity = complexityResult.score;
231+
} catch (error) {
232+
Logger.debug(`Failed to calculate complexity for issue #${issue.number}: ${error}`, DashboardWebviewProvider.ID);
233+
}
234+
}
235+
217236
return {
218237
number: issue.number,
219238
title: issue.title,
@@ -222,7 +241,8 @@ export class DashboardWebviewProvider extends WebviewBase {
222241
state: issue.state,
223242
url: issue.html_url,
224243
createdAt: issue.createdAt,
225-
updatedAt: issue.updatedAt
244+
updatedAt: issue.updatedAt,
245+
complexity
226246
};
227247
}
228248

@@ -282,10 +302,32 @@ export class DashboardWebviewProvider extends WebviewBase {
282302
}
283303

284304
try {
305+
// Try to find the issue in the current repositories
306+
const urlMatch = issueUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\/issues\/(\d+)/);
307+
if (urlMatch) {
308+
const [, owner, repo, issueNumberStr] = urlMatch;
309+
const issueNumber = parseInt(issueNumberStr, 10);
310+
311+
for (const folderManager of this._repositoriesManager.folderManagers) {
312+
const issueModel = await folderManager.resolveIssue(owner, repo, issueNumber);
313+
if (issueModel) {
314+
// Use the extension's command to open the issue description
315+
await vscode.commands.executeCommand('issue.openDescription', issueModel);
316+
return;
317+
}
318+
}
319+
}
320+
321+
// Fallback to opening externally if we can't find the issue locally
285322
await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
286323
} catch (error) {
287324
Logger.error(`Failed to open issue: ${error}`, DashboardWebviewProvider.ID);
288-
vscode.window.showErrorMessage('Failed to open issue.');
325+
// Fallback to opening externally
326+
try {
327+
await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
328+
} catch (fallbackError) {
329+
vscode.window.showErrorMessage('Failed to open issue.');
330+
}
289331
}
290332
}
291333

@@ -341,4 +383,4 @@ export class DashboardWebviewProvider extends WebviewBase {
341383
</body>
342384
</html>`;
343385
}
344-
}
386+
}

src/issues/complexityService.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
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 { IssueModel } from '../github/issueModel';
9+
10+
export interface ComplexityScore {
11+
score: number;
12+
reasoning?: string;
13+
}
14+
15+
export class ComplexityService {
16+
private static readonly ID = 'ComplexityService';
17+
private _cache = new Map<string, ComplexityScore>();
18+
19+
/**
20+
* Calculate complexity score for an issue using VS Code's LM API
21+
* @param issue The issue to calculate complexity for
22+
* @returns A complexity score from 1-100 (1 = simple, 100 = very complex)
23+
*/
24+
async calculateComplexity(issue: IssueModel): Promise<ComplexityScore> {
25+
const cacheKey = `${issue.number}-${issue.updatedAt}`;
26+
27+
// Check cache first
28+
if (this._cache.has(cacheKey)) {
29+
return this._cache.get(cacheKey)!;
30+
}
31+
32+
try {
33+
const models = await vscode.lm.selectChatModels({ vendor: 'copilot' });
34+
if (models.length === 0) {
35+
Logger.debug('No language model available for complexity calculation', ComplexityService.ID);
36+
return { score: 50 }; // Default to medium complexity
37+
}
38+
39+
const model = models[0];
40+
const prompt = this.createComplexityPrompt(issue);
41+
42+
const messages = [
43+
vscode.LanguageModelChatMessage.User(prompt)
44+
];
45+
46+
const request = await model.sendRequest(messages, {
47+
justification: 'Calculating issue complexity to help prioritize developer work'
48+
});
49+
50+
let response = '';
51+
for await (const fragment of request.text) {
52+
response += fragment;
53+
}
54+
55+
const complexityScore = this.parseComplexityResponse(response);
56+
57+
// Cache the result
58+
this._cache.set(cacheKey, complexityScore);
59+
60+
return complexityScore;
61+
} catch (error) {
62+
Logger.error(`Failed to calculate complexity for issue #${issue.number}: ${error}`, ComplexityService.ID);
63+
return { score: 50 }; // Default to medium complexity on error
64+
}
65+
}
66+
67+
/**
68+
* Create a prompt for the language model to analyze issue complexity
69+
*/
70+
private createComplexityPrompt(issue: IssueModel): string {
71+
const labels = issue.item.labels?.map(label => label.name).join(', ') || 'None';
72+
const assignees = issue.assignees?.map(a => a.login).join(', ') || 'None';
73+
74+
return `Analyze the complexity of this GitHub issue and provide a score from 1-100 where:
75+
- 1-20: Very simple (typo fixes, minor documentation updates)
76+
- 21-40: Simple (small feature additions, simple bug fixes)
77+
- 41-60: Medium (moderate features, complex bug fixes)
78+
- 61-80: Complex (large features, architectural changes)
79+
- 81-100: Very complex (major system overhauls, complex integrations)
80+
81+
Issue Details:
82+
Title: ${issue.title}
83+
Description: ${issue.body || 'No description provided'}
84+
Labels: ${labels}
85+
Assignees: ${assignees}
86+
State: ${issue.state}
87+
Milestone: ${issue.milestone?.title || 'None'}
88+
89+
Please respond with ONLY a JSON object in this format:
90+
{
91+
"score": <number between 1-100>,
92+
"reasoning": "<brief explanation of why this score was chosen>"
93+
}`;
94+
}
95+
96+
/**
97+
* Parse the language model response to extract complexity score
98+
*/
99+
private parseComplexityResponse(response: string): ComplexityScore {
100+
try {
101+
// Try to extract JSON from the response
102+
const jsonMatch = response.match(/\{[\s\S]*\}/);
103+
if (jsonMatch) {
104+
const parsed = JSON.parse(jsonMatch[0]);
105+
if (typeof parsed.score === 'number' && parsed.score >= 1 && parsed.score <= 100) {
106+
return {
107+
score: Math.round(parsed.score),
108+
reasoning: parsed.reasoning || undefined
109+
};
110+
}
111+
}
112+
113+
// Fallback: look for just a number in the response
114+
const numberMatch = response.match(/\b(\d{1,3})\b/);
115+
if (numberMatch) {
116+
const score = parseInt(numberMatch[1], 10);
117+
if (score >= 1 && score <= 100) {
118+
return { score };
119+
}
120+
}
121+
122+
Logger.debug(`Could not parse complexity response: ${response}`, ComplexityService.ID);
123+
return { score: 50 };
124+
} catch (error) {
125+
Logger.error(`Error parsing complexity response: ${error}`, ComplexityService.ID);
126+
return { score: 50 };
127+
}
128+
}
129+
130+
/**
131+
* Clear the cache (useful for testing or when issues are updated)
132+
*/
133+
clearCache(): void {
134+
this._cache.clear();
135+
}
136+
137+
/**
138+
* Get cached complexity score if available
139+
*/
140+
getCachedComplexity(issue: IssueModel): ComplexityScore | undefined {
141+
const cacheKey = `${issue.number}-${issue.updatedAt}`;
142+
return this._cache.get(cacheKey);
143+
}
144+
}

src/issues/stateManager.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { IAccount } from '../github/interface';
2525
import { IssueModel } from '../github/issueModel';
2626
import { RepositoriesManager } from '../github/repositoriesManager';
2727
import { getIssueNumberLabel, variableSubstitution } from '../github/utils';
28+
import { ComplexityScore, ComplexityService } from './complexityService';
2829
import { CurrentIssue } from './currentIssue';
2930

3031
const CURRENT_ISSUE_KEY = 'currentIssue';
@@ -50,6 +51,7 @@ const DEFAULT_QUERY_CONFIGURATION_VALUE: { label: string, query: string, groupBy
5051

5152
export class IssueItem extends IssueModel {
5253
uri: vscode.Uri;
54+
complexity?: ComplexityScore;
5355
}
5456

5557
interface SingleRepoState {
@@ -82,6 +84,7 @@ export class StateManager {
8284
public readonly onDidChangeCurrentIssue: vscode.Event<void> = this._onDidChangeCurrentIssue.event;
8385
private initializePromise: Promise<void> | undefined;
8486
private statusBarItem?: vscode.StatusBarItem;
87+
private complexityService: ComplexityService;
8588

8689
getIssueCollection(uri: vscode.Uri): Map<string, Promise<IssueQueryResult>> {
8790
let collection = this._singleRepoStates.get(uri.path)?.issueCollection;
@@ -97,7 +100,9 @@ export class StateManager {
97100
readonly gitAPI: GitApiImpl,
98101
private manager: RepositoriesManager,
99102
private context: vscode.ExtensionContext,
100-
) { }
103+
) {
104+
this.complexityService = new ComplexityService();
105+
}
101106

102107
private getOrCreateSingleRepoState(uri: vscode.Uri, folderManager?: FolderRepositoryManager): SingleRepoState {
103108
let state = this._singleRepoStates.get(uri.path);
@@ -332,13 +337,29 @@ export class StateManager {
332337
return new Promise(async resolve => {
333338
const issues = await folderManager.getIssues(query);
334339
this._onDidChangeIssueData.fire();
335-
resolve(
336-
issues?.items.map(item => {
337-
const issueItem: IssueItem = item as IssueItem;
338-
issueItem.uri = folderManager.repository.rootUri;
339-
return issueItem;
340-
}),
341-
);
340+
const issueItems = issues?.items.map(item => {
341+
const issueItem: IssueItem = item as IssueItem;
342+
issueItem.uri = folderManager.repository.rootUri;
343+
return issueItem;
344+
});
345+
346+
// Calculate complexity scores for all issues
347+
if (issueItems) {
348+
// Calculate complexity scores in the background
349+
Promise.all(issueItems.map(async issueItem => {
350+
try {
351+
issueItem.complexity = await this.complexityService.calculateComplexity(issueItem);
352+
} catch (error) {
353+
// Don't fail the whole operation if complexity calculation fails
354+
console.error(`Failed to calculate complexity for issue #${issueItem.number}:`, error);
355+
}
356+
})).then(() => {
357+
// Fire the change event again after complexity scores are calculated
358+
this._onDidChangeIssueData.fire();
359+
});
360+
}
361+
362+
resolve(issueItems);
342363
});
343364
}
344365

webviews/dashboardView/app.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ interface IssueData {
2727
url: string;
2828
createdAt: string;
2929
updatedAt: string;
30+
complexity?: number;
3031
}
3132

3233
interface DashboardData {
@@ -241,6 +242,11 @@ function Dashboard() {
241242
{/* allow-any-unicode-next-line */}
242243
<span>📅 Updated {formatDate(issue.updatedAt)}</span>
243244
</div>
245+
{issue.complexity && (
246+
<div className="metadata-item complexity-score">
247+
<span>Complexity: {issue.complexity}</span>
248+
</div>
249+
)}
244250
</div>
245251
</div>
246252
))
@@ -308,4 +314,4 @@ function Dashboard() {
308314
</div>
309315
</div >
310316
);
311-
}
317+
}

webviews/dashboardView/index.css

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,21 @@ html, body, app {
201201
text-decoration: underline;
202202
}
203203

204+
.complexity-score {
205+
background-color: var(--vscode-badge-background);
206+
color: var(--vscode-badge-foreground);
207+
padding: 2px 6px;
208+
border-radius: 10px;
209+
font-size: 11px;
210+
font-weight: 500;
211+
}
212+
213+
.complexity-score span {
214+
display: flex;
215+
align-items: center;
216+
gap: 2px;
217+
}
218+
204219
.chat-section {
205220
margin-top: 4px;
206221
flex-shrink: 0;
@@ -303,4 +318,4 @@ html, body, app {
303318
.dashboard-content {
304319
grid-template-columns: 1fr;
305320
}
306-
}
321+
}

0 commit comments

Comments
 (0)