Skip to content

Commit 98b217c

Browse files
committed
polish
1 parent 761e28d commit 98b217c

8 files changed

Lines changed: 110 additions & 38 deletions

File tree

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,11 @@
616616
],
617617
"description": "%githubIssues.createIssueTriggers.description%"
618618
},
619+
"githubPullRequests.codingAgent.codeLens": {
620+
"type": "boolean",
621+
"default": true,
622+
"description": "%githubPullRequests.codingAgent.codeLens.description%"
623+
},
619624
"githubIssues.createInsertFormat": {
620625
"type": "string",
621626
"enum": [

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@
105105
"githubIssues.ignoreMilestones.description": "An array of milestones titles to never show issues from.",
106106
"githubIssues.createIssueTriggers.description": "Strings that will cause the 'Create issue from comment' code action to show.",
107107
"githubIssues.createIssueTriggers.items": "String that enables the 'Create issue from comment' code action. Should not contain whitespace.",
108+
"githubPullRequests.codingAgent.codeLens.description": "Show CodeLens actions above TODO comments for delegating to coding agent.",
108109
"githubIssues.createInsertFormat.description": "Controls whether an issue number (ex. #1234) or a full url (ex. https://github.com/owner/name/issues/1234) is inserted when the Create Issue code action is run.",
109110
"githubIssues.issueCompletions.enabled.description": "Controls whether completion suggestions are shown for issues.",
110111
"githubIssues.userCompletions.enabled.description": "Controls whether completion suggestions are shown for users.",

src/common/settingKeys.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,5 @@ export const COLOR_THEME = 'colorTheme';
9797
export const CODING_AGENT = `${PR_SETTINGS_NAMESPACE}.codingAgent`;
9898
export const CODING_AGENT_ENABLED = 'enabled';
9999
export const CODING_AGENT_AUTO_COMMIT_AND_PUSH = 'autoCommitAndPush';
100-
export const CODING_AGENT_PROMPT_FOR_CONFIRMATION = 'promptForConfirmation';
100+
export const CODING_AGENT_PROMPT_FOR_CONFIRMATION = 'promptForConfirmation';
101+
export const SHOW_CODE_LENS = 'codeLens';

src/common/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,3 +1005,9 @@ export function escapeRegExp(string: string) {
10051005
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
10061006
}
10071007

1008+
export function truncate(value: string, maxLength: number, suffix = '...'): string {
1009+
if (value.length <= maxLength) {
1010+
return value;
1011+
}
1012+
return `${value.substr(0, maxLength)}${suffix}`;
1013+
}

src/github/copilotRemoteAgent.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,8 @@ export class CopilotRemoteAgentManager extends Disposable {
261261
status: CopilotPRStatus;
262262
}[]> | undefined;
263263

264+
private _isAssignable: boolean | undefined;
265+
264266
constructor(
265267
private credentialStore: CredentialStore,
266268
public repositoriesManager: RepositoriesManager,
@@ -287,18 +289,22 @@ export class CopilotRemoteAgentManager extends Disposable {
287289
this._register(this.repositoriesManager.onDidChangeFolderRepositories((event) => {
288290
if (event.added) {
289291
this._register(event.added.onDidChangeAssignableUsers(() => {
292+
this._isAssignable = undefined; // Invalidate cache
290293
this.updateAssignabilityContext();
291294
}));
292295
}
296+
this._isAssignable = undefined; // Invalidate cache
293297
this.updateAssignabilityContext();
294298
}));
295299
this.repositoriesManager.folderManagers.forEach(manager => {
296300
this._register(manager.onDidChangeAssignableUsers(() => {
301+
this._isAssignable = undefined; // Invalidate cache
297302
this.updateAssignabilityContext();
298303
}));
299304
});
300305
this._register(vscode.workspace.onDidChangeConfiguration((e) => {
301306
if (e.affectsConfiguration(CODING_AGENT)) {
307+
this._isAssignable = undefined; // Invalidate cache
302308
this.updateAssignabilityContext();
303309
}
304310
}));
@@ -348,9 +354,18 @@ export class CopilotRemoteAgentManager extends Disposable {
348354
}
349355

350356
async isAssignable(): Promise<boolean> {
357+
const cacheAndReturn = (b: boolean) => {
358+
this._isAssignable = b;
359+
return b;
360+
};
361+
362+
if (this._isAssignable !== undefined) {
363+
return this._isAssignable;
364+
}
365+
351366
const repoInfo = await this.repoInfo();
352367
if (!repoInfo) {
353-
return false;
368+
return cacheAndReturn(false);
354369
}
355370

356371
const { fm } = repoInfo;
@@ -361,14 +376,12 @@ export class CopilotRemoteAgentManager extends Disposable {
361376
const allAssignableUsers = fm.getAllAssignableUsers();
362377

363378
if (!allAssignableUsers) {
364-
return false;
379+
return cacheAndReturn(false);
365380
}
366-
367-
// Check if any of the copilot logins are in the assignable users
368-
return allAssignableUsers.some(user => COPILOT_LOGINS.includes(user.login));
381+
return cacheAndReturn(allAssignableUsers.some(user => COPILOT_LOGINS.includes(user.login)));
369382
} catch (error) {
370383
// If there's an error fetching assignable users, assume not assignable
371-
return false;
384+
return cacheAndReturn(false);
372385
}
373386
}
374387

src/issues/issueFeatureRegistrar.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { chatCommand } from '../lm/utils';
6969
import { ReviewManager } from '../view/reviewManager';
7070
import { ReviewsManager } from '../view/reviewsManager';
7171
import { PRNode } from '../view/treeNodes/pullRequestNode';
72+
import { truncate } from '../common/utils';
7273

7374
const CREATING_ISSUE_FROM_FILE_CONTEXT = 'issues.creatingFromFile';
7475

@@ -147,8 +148,8 @@ export class IssueFeatureRegistrar extends Disposable {
147148
'issue.startCodingAgentFromTodo',
148149
(todoInfo?: { document: vscode.TextDocument; lineNumber: number; line: string; insertIndex: number; range: vscode.Range }) => {
149150
/* __GDPR__
150-
"issue.startCodingAgentFromTodo" : {}
151-
*/
151+
"issue.startCodingAgentFromTodo" : {}
152+
*/
152153
this.telemetry.sendTelemetryEvent('issue.startCodingAgentFromTodo');
153154
return this.startCodingAgentFromTodo(todoInfo);
154155
},
@@ -1492,28 +1493,27 @@ ${options?.body ?? ''}\n
14921493
}
14931494

14941495
const { document, line, insertIndex } = todoInfo;
1495-
1496-
// Extract the TODO text after the trigger word
14971496
const todoText = line.substring(insertIndex).trim();
1498-
14991497
if (!todoText) {
15001498
vscode.window.showWarningMessage(vscode.l10n.t('No task description found in TODO comment'));
15011499
return;
15021500
}
15031501

1504-
// Create a prompt for the coding agent
15051502
const relativePath = vscode.workspace.asRelativePath(document.uri);
15061503
const prompt = vscode.l10n.t('Work on TODO: {0} (from {1})', todoText, relativePath);
1507-
1508-
// Start the coding agent session
1509-
try {
1510-
await this.copilotRemoteAgentManager.commandImpl({
1511-
userPrompt: prompt,
1512-
source: 'todo'
1513-
});
1514-
} catch (error) {
1515-
vscode.window.showErrorMessage(vscode.l10n.t('Failed to start coding agent session: {0}', error.message));
1516-
}
1504+
return vscode.window.withProgress({
1505+
location: vscode.ProgressLocation.Notification,
1506+
title: vscode.l10n.t('Delegating \'{0}\' to coding agent', truncate(todoText, 20))
1507+
}, async (_progress) => {
1508+
try {
1509+
await this.copilotRemoteAgentManager.commandImpl({
1510+
userPrompt: prompt,
1511+
source: 'todo'
1512+
});
1513+
} catch (error) {
1514+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to start coding agent session: {0}', error.message));
1515+
}
1516+
});
15171517
}
15181518

15191519
async assignToCodingAgent(issueModel: any) {

src/issues/issueTodoProvider.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import { MAX_LINE_LENGTH } from './util';
8-
import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys';
8+
import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../common/settingKeys';
99
import { escapeRegExp } from '../common/utils';
1010
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
1111
import { ISSUE_OR_URL_EXPRESSION } from '../github/utils';
@@ -98,32 +98,28 @@ export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.Code
9898
return codeActions;
9999
}
100100

101-
provideCodeLenses(
101+
async provideCodeLenses(
102102
document: vscode.TextDocument,
103103
_token: vscode.CancellationToken,
104-
): vscode.CodeLens[] {
104+
): Promise<vscode.CodeLens[]> {
105105
if (this.expression === undefined) {
106106
return [];
107107
}
108108

109+
// Check if CodeLens is enabled
110+
const isCodeLensEnabled = vscode.workspace.getConfiguration(CODING_AGENT).get(SHOW_CODE_LENS, true);
111+
if (!isCodeLensEnabled) {
112+
return [];
113+
}
114+
109115
const codeLenses: vscode.CodeLens[] = [];
110116
for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) {
111117
const line = document.lineAt(lineNumber).text;
112118
const todoInfo = this.findTodoInLine(lineNumber, line);
113119
if (todoInfo) {
114120
const { match, search, insertIndex } = todoInfo;
115121
const range = new vscode.Range(lineNumber, search, lineNumber, search + match[0].length);
116-
117-
// Create GitHub Issue CodeLens
118-
const createIssueCodeLens = new vscode.CodeLens(range, {
119-
title: vscode.l10n.t('Create GitHub Issue'),
120-
command: 'issue.createIssueFromSelection',
121-
arguments: [{ document, lineNumber, line, insertIndex, range }],
122-
});
123-
codeLenses.push(createIssueCodeLens);
124-
125-
// Delegate to coding agent CodeLens (if copilot manager is available)
126-
if (this.copilotRemoteAgentManager) {
122+
if (this.copilotRemoteAgentManager && (await this.copilotRemoteAgentManager.isAvailable())) {
127123
const startAgentCodeLens = new vscode.CodeLens(range, {
128124
title: vscode.l10n.t('Delegate to coding agent'),
129125
command: 'issue.startCodingAgentFromTodo',

src/test/issues/issueTodoProvider.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('IssueTodoProvider', function () {
6060
lineCount: 4
6161
} as vscode.TextDocument;
6262

63-
const codeLenses = provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
63+
const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
6464

6565
assert.strictEqual(codeLenses.length, 2);
6666

@@ -78,4 +78,54 @@ describe('IssueTodoProvider', function () {
7878
assert.strictEqual(createIssueLens?.range.start.line, 1);
7979
assert.strictEqual(startAgentLens?.range.start.line, 1);
8080
});
81+
82+
it('should respect the createIssueCodeLens setting', async function () {
83+
const mockContext = {
84+
subscriptions: []
85+
} as any as vscode.ExtensionContext;
86+
87+
const mockCopilotManager = {} as any; // Mock CopilotRemoteAgentManager
88+
89+
const provider = new IssueTodoProvider(mockContext, mockCopilotManager);
90+
91+
// Create a mock document with TODO comment
92+
const document = {
93+
lineAt: (line: number) => ({
94+
text: line === 1 ? ' // TODO: Fix this' : 'function test() {}'
95+
}),
96+
lineCount: 4
97+
} as vscode.TextDocument;
98+
99+
// Mock the workspace configuration to return false for createIssueCodeLens
100+
const originalGetConfiguration = vscode.workspace.getConfiguration;
101+
vscode.workspace.getConfiguration = (section?: string) => {
102+
if (section === 'githubIssues') {
103+
return {
104+
get: (key: string, defaultValue?: any) => {
105+
if (key === 'createIssueCodeLens') {
106+
return false;
107+
}
108+
if (key === 'createIssueTriggers') {
109+
return ['TODO', 'todo', 'BUG', 'FIXME', 'ISSUE', 'HACK'];
110+
}
111+
return defaultValue;
112+
}
113+
} as any;
114+
}
115+
return originalGetConfiguration(section);
116+
};
117+
118+
try {
119+
// Update triggers to ensure the expression is set
120+
(provider as any).updateTriggers();
121+
122+
const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token);
123+
124+
// Should return empty array when CodeLens is disabled
125+
assert.strictEqual(codeLenses.length, 0, 'Should not provide code lenses when setting is disabled');
126+
} finally {
127+
// Restore original configuration
128+
vscode.workspace.getConfiguration = originalGetConfiguration;
129+
}
130+
});
81131
});

0 commit comments

Comments
 (0)