From ff31697445e24d66de21b82bdb4efbfdff95a4ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 09:56:01 +0000 Subject: [PATCH 1/6] Initial plan From 5a9a1c9ea0b9d0eef52f3178d8601cc4608d9306 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:03:06 +0000 Subject: [PATCH 2/6] Initial exploration and planning for clickable issue links in @githubpr chat output Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/@types/vscode.proposed.chatSessionsProvider.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index c758688404..60f4552380 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -210,4 +210,4 @@ declare module 'vscode' { */ export function showChatSession(chatSessionType: string, sessionId: string, options: ChatSessionShowOptions): Thenable; } -} \ No newline at end of file +} From de50db6858029c2ad5ef191c13136d1605fb7bac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:18:51 +0000 Subject: [PATCH 3/6] Implement clickable issue links in @githubpr chat output --- webviews/sessionLogView/issueLinker.ts | 78 +++++++++++++++++++++++++ webviews/sessionLogView/sessionView.tsx | 55 ++++++++++------- 2 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 webviews/sessionLogView/issueLinker.ts diff --git a/webviews/sessionLogView/issueLinker.ts b/webviews/sessionLogView/issueLinker.ts new file mode 100644 index 0000000000..8c76102a92 --- /dev/null +++ b/webviews/sessionLogView/issueLinker.ts @@ -0,0 +1,78 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PullInfo } from './messages'; + +// Issue/PR reference patterns - copied from src/github/utils.ts +export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; +export const ISSUE_OR_URL_EXPRESSION = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+)(#issuecomment\-([0-9]+))?)|(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; + +export type ParsedIssue = { + owner: string | undefined; + name: string | undefined; + issueNumber: number; + commentNumber?: number; +}; + +export function parseIssueExpressionOutput(output: RegExpMatchArray | null): ParsedIssue | undefined { + if (!output) { + return undefined; + } + const issue: ParsedIssue = { owner: undefined, name: undefined, issueNumber: 0 }; + if (output.length === 7) { + issue.owner = output[2]; + issue.name = output[3]; + issue.issueNumber = parseInt(output[5]); + return issue; + } else if (output.length === 16) { + issue.owner = output[3] || output[11]; + issue.name = output[4] || output[12]; + issue.issueNumber = parseInt(output[7] || output[14]); + issue.commentNumber = output[9] !== undefined ? parseInt(output[9]) : undefined; + return issue; + } else { + return undefined; + } +} + +export function getIssueNumberLabelFromParsed(parsed: ParsedIssue): string { + if (parsed.owner && parsed.name) { + return `${parsed.owner}/${parsed.name}#${parsed.issueNumber}`; + } + return `#${parsed.issueNumber}`; +} + +/** + * Converts issue/PR references in text to clickable links + * @param text The text to process + * @param pullInfo Repository context for creating links + * @returns The text with issue references converted to markdown links + */ +export async function convertIssueReferencesToLinks(text: string, pullInfo: PullInfo | undefined): Promise { + if (!pullInfo) { + return text; + } + + // Use a simple approach to find and replace issue references + return text.replace(ISSUE_OR_URL_EXPRESSION, (match) => { + const parsed = parseIssueExpressionOutput(match.match(ISSUE_OR_URL_EXPRESSION)); + if (!parsed) { + return match; + } + + // If no owner/name specified, use the current repository context + if (!parsed.owner || !parsed.name) { + parsed.owner = pullInfo.owner; + parsed.name = pullInfo.repo; + } + + const issueNumberLabel = getIssueNumberLabelFromParsed(parsed); + + // Create GitHub URL for the issue/PR + const githubUrl = `https://${pullInfo.host || 'github.com'}/${parsed.owner}/${parsed.name}/issues/${parsed.issueNumber}`; + + return `[${issueNumberLabel}](${githubUrl})`; + }); +} \ No newline at end of file diff --git a/webviews/sessionLogView/sessionView.tsx b/webviews/sessionLogView/sessionView.tsx index 627e139a90..e927db1e35 100644 --- a/webviews/sessionLogView/sessionView.tsx +++ b/webviews/sessionLogView/sessionView.tsx @@ -13,6 +13,7 @@ import { parseDiff, SessionResponseLogChunk, toFileLabel } from '../../common/se import { vscode } from '../common/message'; import { CodeView } from './codeView'; import './index.css'; // Create this file for styling +import { convertIssueReferencesToLinks } from './issueLinker'; import { PullInfo } from './messages'; import { type SessionInfo, type SessionSetupStepResponse } from './sessionsApi'; @@ -30,7 +31,7 @@ export const SessionView: React.FC = (props) => { {props.logs.length === 0 && props.setupSteps && props.setupSteps.length > 0 && ( )} - + {props.info.state === 'in_progress' && !(props.logs.length === 0 && props.setupSteps && props.setupSteps.length > 0) && (
@@ -102,9 +103,10 @@ const SessionHeader: React.FC = ({ info, pullInfo }) => { // Session Log component interface SessionLogProps { readonly logs: readonly SessionResponseLogChunk[]; + readonly pullInfo: PullInfo | undefined; } -const SessionLog: React.FC = ({ logs }) => { +const SessionLog: React.FC = ({ logs, pullInfo }) => { const components = logs.flatMap(x => x.choices).map((choice, index) => { if (!choice.delta.content) { return; @@ -129,6 +131,7 @@ const SessionLog: React.FC = ({ logs }) => { ); } @@ -224,9 +227,10 @@ const SessionLog: React.FC = ({ logs }) => { // Custom component for rendering markdown content interface MarkdownContentProps { content: string; + pullInfo: PullInfo | undefined; } -const MarkdownContent: React.FC = ({ content }) => { +const MarkdownContent: React.FC = ({ content, pullInfo }) => { const containerRef = React.useRef(null); const md = React.useMemo(() => { const mdInstance = new MarkdownIt(); @@ -245,27 +249,36 @@ const MarkdownContent: React.FC = ({ content }) => { React.useEffect(() => { if (!containerRef.current) return; - // Render markdown - containerRef.current.innerHTML = md.render(content); + // Process issue references and convert to clickable links + const processContent = async () => { + const processedContent = await convertIssueReferencesToLinks(content, pullInfo); + + // Render markdown + if (containerRef.current) { + containerRef.current.innerHTML = md.render(processedContent); - // Find all code blocks and render them using CodeView - const codeBlocks = containerRef.current.querySelectorAll('.markdown-code-block'); - codeBlocks.forEach((block) => { - const code = decodeURIComponent(block.getAttribute('data-code') || ''); - const lang = block.getAttribute('data-lang') || 'plaintext'; + // Find all code blocks and render them using CodeView + const codeBlocks = containerRef.current.querySelectorAll('.markdown-code-block'); + codeBlocks.forEach((block) => { + const code = decodeURIComponent(block.getAttribute('data-code') || ''); + const lang = block.getAttribute('data-lang') || 'plaintext'; - const codeViewElement = document.createElement('div'); - block.replaceWith(codeViewElement); + const codeViewElement = document.createElement('div'); + block.replaceWith(codeViewElement); - ReactDOM.render( - , - codeViewElement - ); - }); - }, [content]); + ReactDOM.render( + , + codeViewElement + ); + }); + } + }; + + processContent(); + }, [content, pullInfo, md]); return
; }; From 4a14d64a9544f1f7f95d1277a702e810ff717634 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:27:33 +0000 Subject: [PATCH 4/6] Fix issue linking algorithm to handle multiple references correctly Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- webviews/sessionLogView/issueLinker.ts | 46 ++++++++++++++++---------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/webviews/sessionLogView/issueLinker.ts b/webviews/sessionLogView/issueLinker.ts index 8c76102a92..e7592b9e73 100644 --- a/webviews/sessionLogView/issueLinker.ts +++ b/webviews/sessionLogView/issueLinker.ts @@ -55,24 +55,36 @@ export async function convertIssueReferencesToLinks(text: string, pullInfo: Pull return text; } - // Use a simple approach to find and replace issue references - return text.replace(ISSUE_OR_URL_EXPRESSION, (match) => { - const parsed = parseIssueExpressionOutput(match.match(ISSUE_OR_URL_EXPRESSION)); - if (!parsed) { - return match; - } - - // If no owner/name specified, use the current repository context - if (!parsed.owner || !parsed.name) { - parsed.owner = pullInfo.owner; - parsed.name = pullInfo.repo; - } + // Use a more sophisticated approach similar to findAndModifyString in markdownUtils.ts + let searchResult = text.search(ISSUE_OR_URL_EXPRESSION); + let position = 0; + while (searchResult >= 0 && searchResult < text.length) { + let newBodyFirstPart: string | undefined; + if (searchResult === 0 || text.charAt(searchResult - 1) !== '&') { + const match = text.substring(searchResult).match(ISSUE_OR_URL_EXPRESSION); + if (match) { + const parsed = parseIssueExpressionOutput(match); + if (parsed) { + // If no owner/name specified, use the current repository context + if (!parsed.owner || !parsed.name) { + parsed.owner = pullInfo.owner; + parsed.name = pullInfo.repo; + } - const issueNumberLabel = getIssueNumberLabelFromParsed(parsed); + const issueNumberLabel = getIssueNumberLabelFromParsed(parsed); - // Create GitHub URL for the issue/PR - const githubUrl = `https://${pullInfo.host || 'github.com'}/${parsed.owner}/${parsed.name}/issues/${parsed.issueNumber}`; + // Create GitHub URL for the issue/PR + const githubUrl = `https://${pullInfo.host || 'github.com'}/${parsed.owner}/${parsed.name}/issues/${parsed.issueNumber}`; - return `[${issueNumberLabel}](${githubUrl})`; - }); + const transformed = `[${issueNumberLabel}](${githubUrl})`; + newBodyFirstPart = text.slice(0, searchResult) + transformed; + text = newBodyFirstPart + text.slice(searchResult + match[0].length); + } + } + } + position = newBodyFirstPart ? newBodyFirstPart.length : searchResult + 1; + const newSearchResult = text.substring(position).search(ISSUE_OR_URL_EXPRESSION); + searchResult = newSearchResult > 0 ? position + newSearchResult : newSearchResult; + } + return text; } \ No newline at end of file From a7490c4984360725ec5c1458cdf4199d279b309b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:55:18 +0000 Subject: [PATCH 5/6] Implement issue linking in chat participant (correct location) --- src/extension.ts | 2 +- src/github/markdownUtils.ts | 2 +- src/lm/participants.ts | 48 ++++++++++++- webviews/sessionLogView/issueLinker.ts | 90 ------------------------- webviews/sessionLogView/sessionView.tsx | 55 ++++++--------- 5 files changed, 68 insertions(+), 129 deletions(-) delete mode 100644 webviews/sessionLogView/issueLinker.ts diff --git a/src/extension.ts b/src/extension.ts index 7a49fa5cad..ed072e38b1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -267,7 +267,7 @@ async function init( function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry) { const createParticipant = () => { const chatParticipantState = new ChatParticipantState(); - context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); + context.subscriptions.push(new ChatParticipant(context, chatParticipantState, reposManager)); registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry); }; diff --git a/src/github/markdownUtils.ts b/src/github/markdownUtils.ts index 80b8acc429..58dc9c427f 100644 --- a/src/github/markdownUtils.ts +++ b/src/github/markdownUtils.ts @@ -106,7 +106,7 @@ async function findAndModifyString( return text; } -function findLinksInIssue(body: string, issue: IssueModel): Promise { +export function findLinksInIssue(body: string, issue: IssueModel): Promise { return findAndModifyString(body, ISSUE_OR_URL_EXPRESSION, async (match: RegExpMatchArray) => { const tryParse = parseIssueExpressionOutput(match); if (tryParse) { diff --git a/src/lm/participants.ts b/src/lm/participants.ts index 614eccd7d5..b32f340f0a 100644 --- a/src/lm/participants.ts +++ b/src/lm/participants.ts @@ -7,6 +7,8 @@ import { renderPrompt } from '@vscode/prompt-tsx'; import * as vscode from 'vscode'; import { Disposable } from '../common/lifecycle'; +import { findLinksInIssue } from '../github/markdownUtils'; +import { RepositoriesManager } from '../github/repositoriesManager'; import { ParticipantsPrompt } from './participantsPrompt'; import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils'; @@ -57,7 +59,7 @@ export class ChatParticipantState { export class ChatParticipant extends Disposable { - constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState) { + constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState, private readonly repositoriesManager: RepositoriesManager) { super(); const ghprChatParticipant = this._register(vscode.chat.createChatParticipant('githubpr', ( request: vscode.ChatRequest, @@ -68,6 +70,41 @@ export class ChatParticipant extends Disposable { ghprChatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources/icons/github_logo.png'); } + /** + * Process text to convert issue references to clickable links + */ + private async processIssueReferences(text: string): Promise { + // Get the first folder manager (active workspace) + const folderManagers = this.repositoriesManager.folderManagers; + if (folderManagers.length === 0) { + return text; + } + + const folderManager = folderManagers[0]; + + // Try to use the active pull request as context + const activePullRequest = folderManager.activePullRequest; + if (activePullRequest) { + return await findLinksInIssue(text, activePullRequest); + } + + // If no active PR, try to get the first repository + const repositories = folderManager.gitHubRepositories; + if (repositories.length > 0) { + const repo = repositories[0]; + // Create a minimal issue-like object for the findLinksInIssue function + const mockIssue = { + remote: { + owner: repo.remote.owner, + repositoryName: repo.remote.repositoryName + } + }; + return await findLinksInIssue(text, mockIssue as any); + } + + return text; + } + async handleParticipantRequest( request: vscode.ChatRequest, context: vscode.ChatContext, @@ -123,7 +160,9 @@ export class ChatParticipant extends Disposable { for await (const part of response.stream) { if (part instanceof vscode.LanguageModelTextPart) { - stream.markdown(part.value); + // Process issue references before streaming + const processedText = await this.processIssueReferences(part.value); + stream.markdown(processedText); } else if (part instanceof vscode.LanguageModelToolCallPart) { const tool = vscode.lm.tools.find(tool => tool.name === part.name); @@ -168,7 +207,10 @@ export class ChatParticipant extends Disposable { } if (part.value === TOOL_MARKDOWN_RESULT) { - const markdown = new vscode.MarkdownString((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value); + const markdownText = (toolCallResult.content[++i] as vscode.LanguageModelTextPart).value; + // Process issue references in tool markdown results + const processedMarkdown = await this.processIssueReferences(markdownText); + const markdown = new vscode.MarkdownString(processedMarkdown); markdown.supportHtml = true; stream.markdown(markdown); shownToUser = true; diff --git a/webviews/sessionLogView/issueLinker.ts b/webviews/sessionLogView/issueLinker.ts deleted file mode 100644 index e7592b9e73..0000000000 --- a/webviews/sessionLogView/issueLinker.ts +++ /dev/null @@ -1,90 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { PullInfo } from './messages'; - -// Issue/PR reference patterns - copied from src/github/utils.ts -export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; -export const ISSUE_OR_URL_EXPRESSION = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+)(#issuecomment\-([0-9]+))?)|(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; - -export type ParsedIssue = { - owner: string | undefined; - name: string | undefined; - issueNumber: number; - commentNumber?: number; -}; - -export function parseIssueExpressionOutput(output: RegExpMatchArray | null): ParsedIssue | undefined { - if (!output) { - return undefined; - } - const issue: ParsedIssue = { owner: undefined, name: undefined, issueNumber: 0 }; - if (output.length === 7) { - issue.owner = output[2]; - issue.name = output[3]; - issue.issueNumber = parseInt(output[5]); - return issue; - } else if (output.length === 16) { - issue.owner = output[3] || output[11]; - issue.name = output[4] || output[12]; - issue.issueNumber = parseInt(output[7] || output[14]); - issue.commentNumber = output[9] !== undefined ? parseInt(output[9]) : undefined; - return issue; - } else { - return undefined; - } -} - -export function getIssueNumberLabelFromParsed(parsed: ParsedIssue): string { - if (parsed.owner && parsed.name) { - return `${parsed.owner}/${parsed.name}#${parsed.issueNumber}`; - } - return `#${parsed.issueNumber}`; -} - -/** - * Converts issue/PR references in text to clickable links - * @param text The text to process - * @param pullInfo Repository context for creating links - * @returns The text with issue references converted to markdown links - */ -export async function convertIssueReferencesToLinks(text: string, pullInfo: PullInfo | undefined): Promise { - if (!pullInfo) { - return text; - } - - // Use a more sophisticated approach similar to findAndModifyString in markdownUtils.ts - let searchResult = text.search(ISSUE_OR_URL_EXPRESSION); - let position = 0; - while (searchResult >= 0 && searchResult < text.length) { - let newBodyFirstPart: string | undefined; - if (searchResult === 0 || text.charAt(searchResult - 1) !== '&') { - const match = text.substring(searchResult).match(ISSUE_OR_URL_EXPRESSION); - if (match) { - const parsed = parseIssueExpressionOutput(match); - if (parsed) { - // If no owner/name specified, use the current repository context - if (!parsed.owner || !parsed.name) { - parsed.owner = pullInfo.owner; - parsed.name = pullInfo.repo; - } - - const issueNumberLabel = getIssueNumberLabelFromParsed(parsed); - - // Create GitHub URL for the issue/PR - const githubUrl = `https://${pullInfo.host || 'github.com'}/${parsed.owner}/${parsed.name}/issues/${parsed.issueNumber}`; - - const transformed = `[${issueNumberLabel}](${githubUrl})`; - newBodyFirstPart = text.slice(0, searchResult) + transformed; - text = newBodyFirstPart + text.slice(searchResult + match[0].length); - } - } - } - position = newBodyFirstPart ? newBodyFirstPart.length : searchResult + 1; - const newSearchResult = text.substring(position).search(ISSUE_OR_URL_EXPRESSION); - searchResult = newSearchResult > 0 ? position + newSearchResult : newSearchResult; - } - return text; -} \ No newline at end of file diff --git a/webviews/sessionLogView/sessionView.tsx b/webviews/sessionLogView/sessionView.tsx index e927db1e35..627e139a90 100644 --- a/webviews/sessionLogView/sessionView.tsx +++ b/webviews/sessionLogView/sessionView.tsx @@ -13,7 +13,6 @@ import { parseDiff, SessionResponseLogChunk, toFileLabel } from '../../common/se import { vscode } from '../common/message'; import { CodeView } from './codeView'; import './index.css'; // Create this file for styling -import { convertIssueReferencesToLinks } from './issueLinker'; import { PullInfo } from './messages'; import { type SessionInfo, type SessionSetupStepResponse } from './sessionsApi'; @@ -31,7 +30,7 @@ export const SessionView: React.FC = (props) => { {props.logs.length === 0 && props.setupSteps && props.setupSteps.length > 0 && ( )} - + {props.info.state === 'in_progress' && !(props.logs.length === 0 && props.setupSteps && props.setupSteps.length > 0) && (
@@ -103,10 +102,9 @@ const SessionHeader: React.FC = ({ info, pullInfo }) => { // Session Log component interface SessionLogProps { readonly logs: readonly SessionResponseLogChunk[]; - readonly pullInfo: PullInfo | undefined; } -const SessionLog: React.FC = ({ logs, pullInfo }) => { +const SessionLog: React.FC = ({ logs }) => { const components = logs.flatMap(x => x.choices).map((choice, index) => { if (!choice.delta.content) { return; @@ -131,7 +129,6 @@ const SessionLog: React.FC = ({ logs, pullInfo }) => { ); } @@ -227,10 +224,9 @@ const SessionLog: React.FC = ({ logs, pullInfo }) => { // Custom component for rendering markdown content interface MarkdownContentProps { content: string; - pullInfo: PullInfo | undefined; } -const MarkdownContent: React.FC = ({ content, pullInfo }) => { +const MarkdownContent: React.FC = ({ content }) => { const containerRef = React.useRef(null); const md = React.useMemo(() => { const mdInstance = new MarkdownIt(); @@ -249,36 +245,27 @@ const MarkdownContent: React.FC = ({ content, pullInfo }) React.useEffect(() => { if (!containerRef.current) return; - // Process issue references and convert to clickable links - const processContent = async () => { - const processedContent = await convertIssueReferencesToLinks(content, pullInfo); - - // Render markdown - if (containerRef.current) { - containerRef.current.innerHTML = md.render(processedContent); + // Render markdown + containerRef.current.innerHTML = md.render(content); - // Find all code blocks and render them using CodeView - const codeBlocks = containerRef.current.querySelectorAll('.markdown-code-block'); - codeBlocks.forEach((block) => { - const code = decodeURIComponent(block.getAttribute('data-code') || ''); - const lang = block.getAttribute('data-lang') || 'plaintext'; + // Find all code blocks and render them using CodeView + const codeBlocks = containerRef.current.querySelectorAll('.markdown-code-block'); + codeBlocks.forEach((block) => { + const code = decodeURIComponent(block.getAttribute('data-code') || ''); + const lang = block.getAttribute('data-lang') || 'plaintext'; - const codeViewElement = document.createElement('div'); - block.replaceWith(codeViewElement); + const codeViewElement = document.createElement('div'); + block.replaceWith(codeViewElement); - ReactDOM.render( - , - codeViewElement - ); - }); - } - }; - - processContent(); - }, [content, pullInfo, md]); + ReactDOM.render( + , + codeViewElement + ); + }); + }, [content]); return
; }; From 8ca8f2915f2a4145c78ad63ad9ac4d26f25decd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:55:37 +0000 Subject: [PATCH 6/6] Complete issue linking implementation in chat participant Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/lm/participants.ts | 502 ++++++++++++++++++++--------------------- 1 file changed, 251 insertions(+), 251 deletions(-) diff --git a/src/lm/participants.ts b/src/lm/participants.ts index b32f340f0a..f8a129a59b 100644 --- a/src/lm/participants.ts +++ b/src/lm/participants.ts @@ -1,251 +1,251 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; -import { renderPrompt } from '@vscode/prompt-tsx'; -import * as vscode from 'vscode'; -import { Disposable } from '../common/lifecycle'; -import { findLinksInIssue } from '../github/markdownUtils'; -import { RepositoriesManager } from '../github/repositoriesManager'; -import { ParticipantsPrompt } from './participantsPrompt'; -import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils'; - -export class ChatParticipantState { - private _messages: vscode.LanguageModelChatMessage[] = []; - - get lastToolResult(): (vscode.LanguageModelTextPart | vscode.LanguageModelToolResultPart | vscode.LanguageModelToolCallPart)[] { - for (let i = this._messages.length - 1; i >= 0; i--) { - const message = this._messages[i]; - for (const part of message.content) { - if (part instanceof vscode.LanguageModelToolResultPart) { - return message.content; - } - } - } - return []; - } - - get firstUserMessage(): vscode.LanguageModelTextPart | undefined { - for (let i = 0; i < this._messages.length; i++) { - const message = this._messages[i]; - if (message.role === vscode.LanguageModelChatMessageRole.User && message.content) { - for (const part of message.content) { - if (part instanceof vscode.LanguageModelTextPart) { - return part; - } - } - } - } - } - - get messages(): vscode.LanguageModelChatMessage[] { - return this._messages; - } - - addMessage(message: vscode.LanguageModelChatMessage): void { - this._messages.push(message); - } - - addMessages(messages: vscode.LanguageModelChatMessage[]): void { - this._messages.push(...messages); - } - - reset(): void { - this._messages = []; - } -} - -export class ChatParticipant extends Disposable { - - constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState, private readonly repositoriesManager: RepositoriesManager) { - super(); - const ghprChatParticipant = this._register(vscode.chat.createChatParticipant('githubpr', ( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ) => this.handleParticipantRequest(request, context, stream, token))); - ghprChatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources/icons/github_logo.png'); - } - - /** - * Process text to convert issue references to clickable links - */ - private async processIssueReferences(text: string): Promise { - // Get the first folder manager (active workspace) - const folderManagers = this.repositoriesManager.folderManagers; - if (folderManagers.length === 0) { - return text; - } - - const folderManager = folderManagers[0]; - - // Try to use the active pull request as context - const activePullRequest = folderManager.activePullRequest; - if (activePullRequest) { - return await findLinksInIssue(text, activePullRequest); - } - - // If no active PR, try to get the first repository - const repositories = folderManager.gitHubRepositories; - if (repositories.length > 0) { - const repo = repositories[0]; - // Create a minimal issue-like object for the findLinksInIssue function - const mockIssue = { - remote: { - owner: repo.remote.owner, - repositoryName: repo.remote.repositoryName - } - }; - return await findLinksInIssue(text, mockIssue as any); - } - - return text; - } - - async handleParticipantRequest( - request: vscode.ChatRequest, - context: vscode.ChatContext, - stream: vscode.ChatResponseStream, - token: vscode.CancellationToken - ): Promise { - this.state.reset(); - - const models = await vscode.lm.selectChatModels({ - vendor: 'copilot', - family: 'gpt-4o' - }); - const model = models[0]; - - - const allTools: vscode.LanguageModelChatTool[] = []; - for (const tool of vscode.lm.tools) { - if (request.tools.has(tool.name) && request.tools.get(tool.name)) { - allTools.push(tool); - } else if (tool.name.startsWith('github-pull-request')) { - allTools.push(tool); - } - } - - const { messages } = await renderPrompt( - ParticipantsPrompt, - { userMessage: request.prompt }, - { modelMaxPromptTokens: model.maxInputTokens }, - model); - - this.state.addMessages(messages); - - const toolReferences = [...request.toolReferences]; - const options: vscode.LanguageModelChatRequestOptions = { - justification: 'Answering user questions pertaining to GitHub.' - }; - - const commands: vscode.Command[] = []; - const runWithFunctions = async (): Promise => { - - const requestedTool = toolReferences.shift(); - if (requestedTool) { - options.toolMode = vscode.LanguageModelChatToolMode.Required; - options.tools = allTools.filter(tool => tool.name === requestedTool.name); - } else { - options.toolMode = undefined; - options.tools = allTools; - } - - const toolCalls: IToolCall[] = []; - const response = await model.sendRequest(this.state.messages, options, token); - - for await (const part of response.stream) { - - if (part instanceof vscode.LanguageModelTextPart) { - // Process issue references before streaming - const processedText = await this.processIssueReferences(part.value); - stream.markdown(processedText); - } else if (part instanceof vscode.LanguageModelToolCallPart) { - - const tool = vscode.lm.tools.find(tool => tool.name === part.name); - if (!tool) { - throw new Error('Got invalid tool choice: ' + part.name); - } - - let input: any; - try { - input = part.input; - } catch (err) { - throw new Error(`Got invalid tool use parameters: "${JSON.stringify(part.input)}". (${(err as Error).message})`); - } - - const invocationOptions: vscode.LanguageModelToolInvocationOptions = { input, toolInvocationToken: request.toolInvocationToken }; - toolCalls.push({ - call: part, - result: vscode.lm.invokeTool(tool.name, invocationOptions, token), - tool - }); - } - } - - if (toolCalls.length) { - const assistantMsg = vscode.LanguageModelChatMessage.Assistant(''); - assistantMsg.content = toolCalls.map(toolCall => new vscode.LanguageModelToolCallPart(toolCall.call.callId, toolCall.tool.name, toolCall.call.input)); - this.state.addMessage(assistantMsg); - - let shownToUser = false; - for (const toolCall of toolCalls) { - let toolCallResult = (await toolCall.result); - - const additionalContent: vscode.LanguageModelTextPart[] = []; - let result: vscode.LanguageModelToolResultPart | undefined; - - for (let i = 0; i < toolCallResult.content.length; i++) { - const part = toolCallResult.content[i]; - if (!(part instanceof vscode.LanguageModelTextPart)) { - // We only support text results for now, will change when we finish adopting prompt-tsx - result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, toolCallResult.content); - continue; - } - - if (part.value === TOOL_MARKDOWN_RESULT) { - const markdownText = (toolCallResult.content[++i] as vscode.LanguageModelTextPart).value; - // Process issue references in tool markdown results - const processedMarkdown = await this.processIssueReferences(markdownText); - const markdown = new vscode.MarkdownString(processedMarkdown); - markdown.supportHtml = true; - stream.markdown(markdown); - shownToUser = true; - } else if (part.value === TOOL_COMMAND_RESULT) { - commands.push(JSON.parse((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value) as vscode.Command); - } else { - if (!result) { - result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, [part]); - } else { - additionalContent.push(part); - } - } - } - const message = vscode.LanguageModelChatMessage.User(''); - message.content = [result!]; - this.state.addMessage(message); - if (additionalContent.length) { - const additionalMessage = vscode.LanguageModelChatMessage.User(''); - additionalMessage.content = additionalContent; - this.state.addMessage(additionalMessage); - } - } - - this.state.addMessage(vscode.LanguageModelChatMessage.User(`Above is the result of calling the functions ${toolCalls.map(call => call.tool.name).join(', ')}. ${shownToUser ? 'The user can see the result of the tool call.' : ''}`)); - return runWithFunctions(); - } - }; - await runWithFunctions(); - this.addButtons(stream, commands); - } - - private addButtons(stream: vscode.ChatResponseStream, commands: vscode.Command[]) { - for (const command of commands) { - stream.button(command); - } - } -} - +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; +import { renderPrompt } from '@vscode/prompt-tsx'; +import * as vscode from 'vscode'; +import { Disposable } from '../common/lifecycle'; +import { findLinksInIssue } from '../github/markdownUtils'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { ParticipantsPrompt } from './participantsPrompt'; +import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils'; + +export class ChatParticipantState { + private _messages: vscode.LanguageModelChatMessage[] = []; + + get lastToolResult(): (vscode.LanguageModelTextPart | vscode.LanguageModelToolResultPart | vscode.LanguageModelToolCallPart)[] { + for (let i = this._messages.length - 1; i >= 0; i--) { + const message = this._messages[i]; + for (const part of message.content) { + if (part instanceof vscode.LanguageModelToolResultPart) { + return message.content; + } + } + } + return []; + } + + get firstUserMessage(): vscode.LanguageModelTextPart | undefined { + for (let i = 0; i < this._messages.length; i++) { + const message = this._messages[i]; + if (message.role === vscode.LanguageModelChatMessageRole.User && message.content) { + for (const part of message.content) { + if (part instanceof vscode.LanguageModelTextPart) { + return part; + } + } + } + } + } + + get messages(): vscode.LanguageModelChatMessage[] { + return this._messages; + } + + addMessage(message: vscode.LanguageModelChatMessage): void { + this._messages.push(message); + } + + addMessages(messages: vscode.LanguageModelChatMessage[]): void { + this._messages.push(...messages); + } + + reset(): void { + this._messages = []; + } +} + +export class ChatParticipant extends Disposable { + + constructor(context: vscode.ExtensionContext, private readonly state: ChatParticipantState, private readonly repositoriesManager: RepositoriesManager) { + super(); + const ghprChatParticipant = this._register(vscode.chat.createChatParticipant('githubpr', ( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ) => this.handleParticipantRequest(request, context, stream, token))); + ghprChatParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'resources/icons/github_logo.png'); + } + + /** + * Process text to convert issue references to clickable links + */ + private async processIssueReferences(text: string): Promise { + // Get the first folder manager (active workspace) + const folderManagers = this.repositoriesManager.folderManagers; + if (folderManagers.length === 0) { + return text; + } + + const folderManager = folderManagers[0]; + + // Try to use the active pull request as context + const activePullRequest = folderManager.activePullRequest; + if (activePullRequest) { + return await findLinksInIssue(text, activePullRequest); + } + + // If no active PR, try to get the first repository + const repositories = folderManager.gitHubRepositories; + if (repositories.length > 0) { + const repo = repositories[0]; + // Create a minimal issue-like object for the findLinksInIssue function + const mockIssue = { + remote: { + owner: repo.remote.owner, + repositoryName: repo.remote.repositoryName + } + }; + return await findLinksInIssue(text, mockIssue as any); + } + + return text; + } + + async handleParticipantRequest( + request: vscode.ChatRequest, + context: vscode.ChatContext, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken + ): Promise { + this.state.reset(); + + const models = await vscode.lm.selectChatModels({ + vendor: 'copilot', + family: 'gpt-4o' + }); + const model = models[0]; + + + const allTools: vscode.LanguageModelChatTool[] = []; + for (const tool of vscode.lm.tools) { + if (request.tools.has(tool.name) && request.tools.get(tool.name)) { + allTools.push(tool); + } else if (tool.name.startsWith('github-pull-request')) { + allTools.push(tool); + } + } + + const { messages } = await renderPrompt( + ParticipantsPrompt, + { userMessage: request.prompt }, + { modelMaxPromptTokens: model.maxInputTokens }, + model); + + this.state.addMessages(messages); + + const toolReferences = [...request.toolReferences]; + const options: vscode.LanguageModelChatRequestOptions = { + justification: 'Answering user questions pertaining to GitHub.' + }; + + const commands: vscode.Command[] = []; + const runWithFunctions = async (): Promise => { + + const requestedTool = toolReferences.shift(); + if (requestedTool) { + options.toolMode = vscode.LanguageModelChatToolMode.Required; + options.tools = allTools.filter(tool => tool.name === requestedTool.name); + } else { + options.toolMode = undefined; + options.tools = allTools; + } + + const toolCalls: IToolCall[] = []; + const response = await model.sendRequest(this.state.messages, options, token); + + for await (const part of response.stream) { + + if (part instanceof vscode.LanguageModelTextPart) { + // Process issue references before streaming + const processedText = await this.processIssueReferences(part.value); + stream.markdown(processedText); + } else if (part instanceof vscode.LanguageModelToolCallPart) { + + const tool = vscode.lm.tools.find(tool => tool.name === part.name); + if (!tool) { + throw new Error('Got invalid tool choice: ' + part.name); + } + + let input: any; + try { + input = part.input; + } catch (err) { + throw new Error(`Got invalid tool use parameters: "${JSON.stringify(part.input)}". (${(err as Error).message})`); + } + + const invocationOptions: vscode.LanguageModelToolInvocationOptions = { input, toolInvocationToken: request.toolInvocationToken }; + toolCalls.push({ + call: part, + result: vscode.lm.invokeTool(tool.name, invocationOptions, token), + tool + }); + } + } + + if (toolCalls.length) { + const assistantMsg = vscode.LanguageModelChatMessage.Assistant(''); + assistantMsg.content = toolCalls.map(toolCall => new vscode.LanguageModelToolCallPart(toolCall.call.callId, toolCall.tool.name, toolCall.call.input)); + this.state.addMessage(assistantMsg); + + let shownToUser = false; + for (const toolCall of toolCalls) { + let toolCallResult = (await toolCall.result); + + const additionalContent: vscode.LanguageModelTextPart[] = []; + let result: vscode.LanguageModelToolResultPart | undefined; + + for (let i = 0; i < toolCallResult.content.length; i++) { + const part = toolCallResult.content[i]; + if (!(part instanceof vscode.LanguageModelTextPart)) { + // We only support text results for now, will change when we finish adopting prompt-tsx + result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, toolCallResult.content); + continue; + } + + if (part.value === TOOL_MARKDOWN_RESULT) { + const markdownText = (toolCallResult.content[++i] as vscode.LanguageModelTextPart).value; + // Process issue references in tool markdown results + const processedMarkdown = await this.processIssueReferences(markdownText); + const markdown = new vscode.MarkdownString(processedMarkdown); + markdown.supportHtml = true; + stream.markdown(markdown); + shownToUser = true; + } else if (part.value === TOOL_COMMAND_RESULT) { + commands.push(JSON.parse((toolCallResult.content[++i] as vscode.LanguageModelTextPart).value) as vscode.Command); + } else { + if (!result) { + result = new vscode.LanguageModelToolResultPart(toolCall.call.callId, [part]); + } else { + additionalContent.push(part); + } + } + } + const message = vscode.LanguageModelChatMessage.User(''); + message.content = [result!]; + this.state.addMessage(message); + if (additionalContent.length) { + const additionalMessage = vscode.LanguageModelChatMessage.User(''); + additionalMessage.content = additionalContent; + this.state.addMessage(additionalMessage); + } + } + + this.state.addMessage(vscode.LanguageModelChatMessage.User(`Above is the result of calling the functions ${toolCalls.map(call => call.tool.name).join(', ')}. ${shownToUser ? 'The user can see the result of the tool call.' : ''}`)); + return runWithFunctions(); + } + }; + await runWithFunctions(); + this.addButtons(stream, commands); + } + + private addButtons(stream: vscode.ChatResponseStream, commands: vscode.Command[]) { + for (const command of commands) { + stream.button(command); + } + } +} +