Skip to content

Commit 4b13526

Browse files
Copilotalexr00
andcommitted
Move link conversion to participants.ts and add support for 'pull request' pattern
Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent e594b60 commit 4b13526

File tree

6 files changed

+68
-32
lines changed

6 files changed

+68
-32
lines changed

src/common/uri.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,7 +796,7 @@ export function resolvePath(from: vscode.Uri, to: string) {
796796

797797
/**
798798
* Converts issue and PR number references in text to clickable markdown links.
799-
* Matches patterns like "#123", "issue 123", "issue #123", "PR 123", "PR #123"
799+
* Matches patterns like "#123", "issue 123", "issue #123", "PR 123", "PR #123", "pull request 123", "pull request #123"
800800
* @param text The text to process
801801
* @param owner The repository owner
802802
* @param repo The repository name
@@ -807,8 +807,9 @@ export function convertIssuePRReferencesToLinks(text: string, owner: string, rep
807807
// - #123 (standalone hash with number)
808808
// - issue 123 or issue #123 (case-insensitive)
809809
// - PR 123 or PR #123 (case-insensitive)
810+
// - pull request 123 or pull request #123 (case-insensitive)
810811
// Uses word boundaries to avoid matching in the middle of words
811-
const pattern = /\b(?:issue\s+#?|PR\s+#?|#)(\d+)\b/gi;
812+
const pattern = /\b(?:pull\s+request\s+#?|issue\s+#?|PR\s+#?|#)(\d+)\b/gi;
812813

813814
return text.replace(pattern, (match, number) => {
814815
const url = `https://github.com/${owner}/${repo}/issues/${number}`;

src/extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ async function init(
296296
function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) {
297297
const createParticipant = () => {
298298
const chatParticipantState = new ChatParticipantState();
299-
context.subscriptions.push(new ChatParticipant(context, chatParticipantState));
299+
context.subscriptions.push(new ChatParticipant(context, chatParticipantState, reposManager));
300300
registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel);
301301
};
302302

src/github/copilotRemoteAgent.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import Logger from '../common/logger';
2121
import { GitHubRemote } from '../common/remote';
2222
import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH } from '../common/settingKeys';
2323
import { ITelemetry } from '../common/telemetry';
24-
import { convertIssuePRReferencesToLinks, toOpenPullRequestWebviewUri } from '../common/uri';
24+
import { toOpenPullRequestWebviewUri } from '../common/uri';
2525
import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder';
2626
import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager';
2727
import { extractTitle, formatBodyPlaceholder, truncatePrompt } from './copilotRemoteAgentUtils';
@@ -1246,12 +1246,7 @@ export class CopilotRemoteAgentManager extends Disposable {
12461246
} else {
12471247
if (delta.content) {
12481248
if (!delta.content.startsWith('<pr_title>')) {
1249-
const convertedContent = convertIssuePRReferencesToLinks(
1250-
delta.content,
1251-
pullRequest.remote.owner,
1252-
pullRequest.remote.repositoryName
1253-
);
1254-
stream.markdown(convertedContent);
1249+
stream.markdown(delta.content);
12551250
hasStreamedContent = true;
12561251
}
12571252
}

src/github/copilotRemoteAgent/chatSessionContentBuilder.ts

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from
1010
import { COPILOT_SWE_AGENT } from '../../common/copilot';
1111
import Logger from '../../common/logger';
1212
import { CommentEvent, CopilotFinishedEvent, CopilotStartedEvent, EventType, ReviewEvent, TimelineEvent } from '../../common/timelineEvent';
13-
import { convertIssuePRReferencesToLinks, toOpenPullRequestWebviewUri } from '../../common/uri';
13+
import { toOpenPullRequestWebviewUri } from '../../common/uri';
1414
import { InMemFileChangeModel, RemoteFileChangeModel } from '../../view/fileChangeModel';
1515
import { AssistantDelta, Choice, ToolCall } from '../common';
1616
import { CopilotApi, SessionInfo } from '../copilotApi';
@@ -288,12 +288,7 @@ export class ChatSessionContentBuilder {
288288
}
289289

290290
if (currentResponseContent.trim()) {
291-
const convertedContent = convertIssuePRReferencesToLinks(
292-
currentResponseContent.trim(),
293-
pullRequest.remote.owner,
294-
pullRequest.remote.repositoryName
295-
);
296-
responseParts.push(new vscode.ChatResponseMarkdownPart(convertedContent));
291+
responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim()));
297292
}
298293

299294
if (session.state === 'completed' || session.state === 'failed' /** session can fail with proposed changes */) {
@@ -341,12 +336,7 @@ export class ChatSessionContentBuilder {
341336
if (delta.content && delta.content.trim()) {
342337
// Add any accumulated content as markdown first
343338
if (currentResponseContent.trim()) {
344-
const convertedContent = convertIssuePRReferencesToLinks(
345-
currentResponseContent.trim(),
346-
pullRequest.remote.owner,
347-
pullRequest.remote.repositoryName
348-
);
349-
responseParts.push(new vscode.ChatResponseMarkdownPart(convertedContent));
339+
responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim()));
350340
currentResponseContent = '';
351341
}
352342

@@ -367,12 +357,7 @@ export class ChatSessionContentBuilder {
367357
if (delta.tool_calls) {
368358
// Add any accumulated content as markdown first
369359
if (currentResponseContent.trim()) {
370-
const convertedContent = convertIssuePRReferencesToLinks(
371-
currentResponseContent.trim(),
372-
pullRequest.remote.owner,
373-
pullRequest.remote.repositoryName
374-
);
375-
responseParts.push(new vscode.ChatResponseMarkdownPart(convertedContent));
360+
responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim()));
376361
currentResponseContent = '';
377362
}
378363

src/lm/participants.ts

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import * as vscode from 'vscode';
99
import { ParticipantsPrompt } from './participantsPrompt';
1010
import { Disposable } from '../common/lifecycle';
1111
import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils';
12+
import { convertIssuePRReferencesToLinks } from '../common/uri';
13+
import { RepositoriesManager } from '../github/repositoriesManager';
1214

1315
export class ChatParticipantState {
1416
private _messages: vscode.LanguageModelChatMessage[] = [];
@@ -58,7 +60,7 @@ export class ChatParticipantState {
5860

5961
export class ChatParticipant extends Disposable {
6062

61-
constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState) {
63+
constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState, private readonly repositoriesManager: RepositoriesManager) {
6264
super();
6365
const ghprChatParticipant = this._register(vscode.chat.createChatParticipant('githubpr', (
6466
request: vscode.ChatRequest,
@@ -124,7 +126,12 @@ export class ChatParticipant extends Disposable {
124126
for await (const part of response.stream) {
125127

126128
if (part instanceof vscode.LanguageModelTextPart) {
127-
stream.markdown(part.value);
129+
const repoInfo = this.getActiveRepositoryInfo();
130+
let markdown = part.value;
131+
if (repoInfo) {
132+
markdown = convertIssuePRReferencesToLinks(markdown, repoInfo.owner, repoInfo.repo);
133+
}
134+
stream.markdown(markdown);
128135
} else if (part instanceof vscode.LanguageModelToolCallPart) {
129136

130137
const tool = vscode.lm.tools.find(tool => tool.name === part.name);
@@ -169,7 +176,12 @@ export class ChatParticipant extends Disposable {
169176
}
170177

171178
if (part.value === TOOL_MARKDOWN_RESULT) {
172-
const markdown = new vscode.MarkdownString((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value);
179+
const repoInfo = this.getActiveRepositoryInfo();
180+
let markdownText = (toolCallResult.content[++i] as vscode.LanguageModelTextPart).value;
181+
if (repoInfo) {
182+
markdownText = convertIssuePRReferencesToLinks(markdownText, repoInfo.owner, repoInfo.repo);
183+
}
184+
const markdown = new vscode.MarkdownString(markdownText);
173185
markdown.supportHtml = true;
174186
stream.markdown(markdown);
175187
shownToUser = true;
@@ -206,5 +218,30 @@ export class ChatParticipant extends Disposable {
206218
stream.button(command);
207219
}
208220
}
221+
222+
private getActiveRepositoryInfo(): { owner: string; repo: string } | undefined {
223+
// Try to get repository info from the active pull request first
224+
const folderManager = this.repositoriesManager.folderManagers.find((manager) => manager.activePullRequest);
225+
if (folderManager?.activePullRequest) {
226+
return {
227+
owner: folderManager.activePullRequest.remote.owner,
228+
repo: folderManager.activePullRequest.remote.repositoryName
229+
};
230+
}
231+
232+
// Fallback to the first available folder manager with a GitHub repository
233+
if (this.repositoriesManager.folderManagers.length > 0) {
234+
const firstManager = this.repositoriesManager.folderManagers[0];
235+
const ghRepo = firstManager.gitHubRepositories[0];
236+
if (ghRepo) {
237+
return {
238+
owner: ghRepo.remote.owner,
239+
repo: ghRepo.remote.repositoryName
240+
};
241+
}
242+
}
243+
244+
return undefined;
245+
}
209246
}
210247

src/test/common/uri.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,23 @@ describe('uri', () => {
180180
const result = convertIssuePRReferencesToLinks(text, owner, repo);
181181
assert.strictEqual(result, '');
182182
});
183+
184+
it('should convert pull request references with # prefix', () => {
185+
const text = 'See pull request #789 for details.';
186+
const result = convertIssuePRReferencesToLinks(text, owner, repo);
187+
assert.strictEqual(result, 'See [pull request #789](https://github.com/microsoft/vscode-pull-request-github/issues/789) for details.');
188+
});
189+
190+
it('should convert pull request references without # prefix', () => {
191+
const text = 'Related to pull request 456.';
192+
const result = convertIssuePRReferencesToLinks(text, owner, repo);
193+
assert.strictEqual(result, 'Related to [pull request 456](https://github.com/microsoft/vscode-pull-request-github/issues/456).');
194+
});
195+
196+
it('should handle case-insensitive pull request keyword', () => {
197+
const text = 'See Pull Request #100 and PULL REQUEST 200.';
198+
const result = convertIssuePRReferencesToLinks(text, owner, repo);
199+
assert.strictEqual(result, 'See [Pull Request #100](https://github.com/microsoft/vscode-pull-request-github/issues/100) and [PULL REQUEST 200](https://github.com/microsoft/vscode-pull-request-github/issues/200).');
200+
});
183201
});
184202
});

0 commit comments

Comments
 (0)