From 8ccf591cb73d3feb0a36f633e27ec56cd51858a9 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Tue, 24 Jun 2025 13:21:49 +0100 Subject: [PATCH 1/4] Assistant: Add getChangedFiles tool --- extensions/positron-assistant/package.json | 16 +++ extensions/positron-assistant/src/git.ts | 109 +++++++++++++++++++++ extensions/positron-assistant/src/tools.ts | 16 +++ extensions/positron-assistant/src/types.ts | 1 + 4 files changed, 142 insertions(+) create mode 100644 extensions/positron-assistant/src/git.ts diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index 8bc876c85a17..fdc291e696b7 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -326,6 +326,22 @@ } } } + }, + { + "name": "getChangedFiles", + "displayName": "Get changed files", + "modelDescription": "Get summaries and git diffs for current changes to files in this workspace.", + "canBeReferencedInPrompt": true, + "userDescription": "Get changed files", + "toolReferenceName": "changes", + "icon": "$(diff)", + "tags": [ + "positron-assistant" + ], + "inputSchema": { + "type": "object", + "properties": {} + } } ] }, diff --git a/extensions/positron-assistant/src/git.ts b/extensions/positron-assistant/src/git.ts new file mode 100644 index 000000000000..5d50f10eefeb --- /dev/null +++ b/extensions/positron-assistant/src/git.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2025 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; + +import { GitExtension, Repository, Status, Change } from '../../git/src/api/git.js'; + +export enum GitRepoChangeKind { + Staged = 'staged', + Unstaged = 'unstaged', + Merge = 'merge', + Untracked = 'untracked', + All = 'all', +} + +export interface GitRepoChangeSummary { + uri: vscode.Uri; + summary: string; +} + +export interface GitRepoChange { + repo: Repository; + changes: GitRepoChangeSummary[]; +} + +/** Get the list of active repositories */ +function currentGitRepositories(): Repository[] { + // Obtain a handle to git extension API + const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; + if (!gitExtension) { + throw new Error('Git extension not found'); + } + const git = gitExtension.getAPI(1); + if (git.repositories.length === 0) { + throw new Error('No Git repositories found'); + } + + return git.repositories; +} + +/** Summarise the content of a git change */ +async function gitChangeSummary(repo: Repository, change: Change, kind: GitRepoChangeKind): Promise { + const uri = change.uri.fsPath.replace(repo.rootUri.fsPath, ''); + const originalUri = change.originalUri.fsPath.replace(repo.rootUri.fsPath, ''); + const renameUri = change.renameUri?.fsPath.replace(repo.rootUri.fsPath, ''); + switch (change.status) { + // File-level changes + case Status.INDEX_ADDED: + case Status.UNTRACKED: + return { uri: change.uri, summary: `Added: ${uri}` }; + case Status.INDEX_DELETED: + case Status.DELETED: + return { uri: change.uri, summary: `Deleted: ${uri}` }; + case Status.INDEX_RENAMED: + return { uri: change.uri, summary: `Renamed: ${originalUri} to ${renameUri}` }; + case Status.INDEX_COPIED: + return { uri: change.uri, summary: `Copied: ${originalUri} to ${uri}` }; + case Status.IGNORED: + return { uri: change.uri, summary: `Ignored: ${uri}` }; + default: { + // Otherwise, git diff text content for this file + if (kind === GitRepoChangeKind.Staged) { + const diff = await repo.diffIndexWithHEAD(change.uri.fsPath); + return { uri: change.uri, summary: `Modified:\n${diff}` }; + } else { + const diff = await repo.diffWithHEAD(change.uri.fsPath); + return { uri: change.uri, summary: `Modified:\n${diff}` }; + } + } + } +} + +/** Get current workspace git repository changes as text summaries */ +export async function getWorkspaceGitChanges(kind: GitRepoChangeKind): Promise { + const repos = currentGitRepositories(); + + // Combine and summarise each kind of git repo change + return Promise.all(repos.map(async (repo) => { + const stateChanges: { change: Change; kind: GitRepoChangeKind }[] = []; + + if (kind === GitRepoChangeKind.Staged || kind === GitRepoChangeKind.All) { + stateChanges.push(...repo.state.indexChanges.map((change) => { + return { change, kind: GitRepoChangeKind.Staged }; + })); + } + if (kind === GitRepoChangeKind.Unstaged || kind === GitRepoChangeKind.All) { + stateChanges.push(...repo.state.workingTreeChanges.map((change) => { + return { change, kind: GitRepoChangeKind.Unstaged }; + })); + } + if (kind === GitRepoChangeKind.Untracked || kind === GitRepoChangeKind.All) { + stateChanges.push(...repo.state.untrackedChanges.map((change) => { + return { change, kind: GitRepoChangeKind.Untracked }; + })); + } + if (kind === GitRepoChangeKind.Merge || kind === GitRepoChangeKind.All) { + stateChanges.push(...repo.state.mergeChanges.map((change) => { + return { change, kind: GitRepoChangeKind.Merge }; + })); + } + + const changes = await Promise.all(stateChanges.map(async (state) => { + return gitChangeSummary(repo, state.change, state.kind); + })); + return { repo, changes }; + })); +} diff --git a/extensions/positron-assistant/src/tools.ts b/extensions/positron-assistant/src/tools.ts index 1f3b310f545a..61c0fe20ee1f 100644 --- a/extensions/positron-assistant/src/tools.ts +++ b/extensions/positron-assistant/src/tools.ts @@ -9,6 +9,7 @@ import { LanguageModelImage } from './languageModelParts.js'; import { ParticipantService } from './participants.js'; import { PositronAssistantToolName } from './types.js'; import { ProjectTreeTool } from './tools/projectTreeTool.js'; +import { getWorkspaceGitChanges, GitRepoChangeKind } from './git.js'; /** @@ -283,6 +284,21 @@ export function registerAssistantTools( context.subscriptions.push(inspectVariablesTool); context.subscriptions.push(ProjectTreeTool); + + const getChangedFilesTool = vscode.lm.registerTool<{}>(PositronAssistantToolName.GetChangedFiles, { + invoke: async (options, token) => { + const repoChanges = await getWorkspaceGitChanges(GitRepoChangeKind.All); + const textChanges = repoChanges.map((({ changes }) => { + return changes.map((change) => change.summary).join('\n'); + })).join('\n\n'); + + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(textChanges) + ]); + }, + }); + + context.subscriptions.push(getChangedFilesTool); } /** diff --git a/extensions/positron-assistant/src/types.ts b/extensions/positron-assistant/src/types.ts index d743eebdab2f..aee473978f62 100644 --- a/extensions/positron-assistant/src/types.ts +++ b/extensions/positron-assistant/src/types.ts @@ -11,4 +11,5 @@ export enum PositronAssistantToolName { InspectVariables = 'inspectVariables', SelectionEdit = 'selectionEdit', ProjectTree = 'getProjectTree', + GetChangedFiles = 'getChangedFiles', } From 7fb1acc05a89c63bacb7e08622c60f1740ba2520 Mon Sep 17 00:00:00 2001 From: George Stagg Date: Tue, 24 Jun 2025 13:31:09 +0100 Subject: [PATCH 2/4] Assistant: Generate commit message --- extensions/positron-assistant/package.json | 28 ++++++++++ .../positron-assistant/package.nls.json | 2 + .../positron-assistant/src/extension.ts | 10 ++++ extensions/positron-assistant/src/git.ts | 56 ++++++++++++++++++- .../src/md/prompts/git/commit.md | 3 + 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 extensions/positron-assistant/src/md/prompts/git/commit.md diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index fdc291e696b7..5cac162b25ef 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -104,6 +104,20 @@ ] } ], + "menus": { + "scm/inputBox": [ + { + "command": "positron-assistant.generateCommitMessage", + "group": "navigation", + "when": "!positron-assistant.generatingCommitMessage" + }, + { + "command": "positron-assistant.cancelGenerateCommitMessage", + "group": "navigation", + "when": "positron-assistant.generatingCommitMessage" + } + ] + }, "commands": [ { "command": "positron-assistant.configureModels", @@ -116,6 +130,20 @@ "title": "%commands.logStoredModels.title%", "category": "%commands.category%", "enablement": "config.positron.assistant.enable" + }, + { + "command": "positron-assistant.generateCommitMessage", + "title": "%commands.generateCommitMessage.title%", + "category": "%commands.category%", + "enablement": "config.positron.assistant.enable", + "icon": "$(sparkle)" + }, + { + "command": "positron-assistant.cancelGenerateCommitMessage", + "title": "%commands.cancelGenerateCommitMessage.title%", + "category": "%commands.category%", + "enablement": "config.positron.assistant.enable", + "icon": "$(stop)" } ], "configuration": [ diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index 91886d5d4d71..8fe0d35e39d1 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -3,6 +3,8 @@ "description": "Provides default assistant and language models for Positron.", "commands.configureModels.title": "Configure Language Model Providers", "commands.logStoredModels.title": "Log the stored language models", + "commands.generateCommitMessage.title": "Generate Commit Message", + "commands.cancelGenerateCommitMessage.title": "Cancel Generate Commit Message", "commands.copilot.signin.title": "Copilot Sign In", "commands.copilot.signout.title": "Copilot Sign Out", "commands.category": "Positron Assistant", diff --git a/extensions/positron-assistant/src/extension.ts b/extensions/positron-assistant/src/extension.ts index 5bd28e97c992..540e4d7f5d7b 100644 --- a/extensions/positron-assistant/src/extension.ts +++ b/extensions/positron-assistant/src/extension.ts @@ -14,6 +14,7 @@ import { registerAssistantTools } from './tools.js'; import { registerCopilotService } from './copilot.js'; import { ALL_DOCUMENTS_SELECTOR, DEFAULT_MAX_TOKEN_OUTPUT } from './constants.js'; import { registerCodeActionProvider } from './codeActions.js'; +import { generateCommitMessage } from './git.js'; const hasChatModelsContextKey = 'positron-assistant.hasChatModels'; @@ -208,6 +209,14 @@ function registerConfigureModelsCommand(context: vscode.ExtensionContext, storag ); } +function registerGenerateCommitMessageCommand(context: vscode.ExtensionContext) { + context.subscriptions.push( + vscode.commands.registerCommand('positron-assistant.generateCommitMessage', () => { + generateCommitMessage(context); + }) + ); +} + function registerAssistant(context: vscode.ExtensionContext) { // Initialize secret storage. In web mode, we currently need to use global @@ -230,6 +239,7 @@ function registerAssistant(context: vscode.ExtensionContext) { // Commands registerConfigureModelsCommand(context, storage); + registerGenerateCommitMessageCommand(context); // Register mapped edits provider registerMappedEditsProvider(context, participantService, log); diff --git a/extensions/positron-assistant/src/git.ts b/extensions/positron-assistant/src/git.ts index 5d50f10eefeb..79c788f49afd 100644 --- a/extensions/positron-assistant/src/git.ts +++ b/extensions/positron-assistant/src/git.ts @@ -4,8 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import * as fs from 'fs'; import { GitExtension, Repository, Status, Change } from '../../git/src/api/git.js'; +import { EXTENSION_ROOT_DIR } from './constants'; + +const mdDir = `${EXTENSION_ROOT_DIR}/src/md/`; +const generatingGitCommitKey = 'positron-assistant.generatingCommitMessage'; export enum GitRepoChangeKind { Staged = 'staged', @@ -77,7 +82,7 @@ export async function getWorkspaceGitChanges(kind: GitRepoChangeKind): Promise { + const repoChanges = await Promise.all(repos.map(async (repo) => { const stateChanges: { change: Change; kind: GitRepoChangeKind }[] = []; if (kind === GitRepoChangeKind.Staged || kind === GitRepoChangeKind.All) { @@ -106,4 +111,53 @@ export async function getWorkspaceGitChanges(kind: GitRepoChangeKind): Promise repoChange.changes.length > 0); +} + +/** Generate a commit message for git repositories with staged changes */ +export async function generateCommitMessage(context: vscode.ExtensionContext) { + await vscode.commands.executeCommand('setContext', generatingGitCommitKey, true); + + const models = (await vscode.lm.selectChatModels()).filter((model) => { + return model.family !== 'echo' && model.family !== 'error'; + }); + if (models.length === 0) { + vscode.commands.executeCommand('setContext', generatingGitCommitKey, false); + throw new Error('No language models available for commit message generation.'); + } + const model = models[0]; + + const tokenSource: vscode.CancellationTokenSource = new vscode.CancellationTokenSource(); + const cancelDisposable = vscode.commands.registerCommand('positron-assistant.cancelGenerateCommitMessage', () => { + tokenSource.cancel(); + vscode.commands.executeCommand('setContext', generatingGitCommitKey, false); + }); + + // Send repo changes to the LLM and update the commit message input boxes + const allChanges = await getWorkspaceGitChanges(GitRepoChangeKind.All); + const stagedChanges = await getWorkspaceGitChanges(GitRepoChangeKind.Staged); + const gitChanges = stagedChanges.length > 0 ? stagedChanges : allChanges; + + const system: string = await fs.promises.readFile(`${mdDir}/prompts/git/commit.md`, 'utf8'); + try { + await Promise.all(gitChanges.map(async ({ repo, changes }) => { + if (changes.length > 0) { + const response = await model.sendRequest([ + vscode.LanguageModelChatMessage.User(changes.map(change => change.summary).join('\n')), + ], { modelOptions: { system } }, tokenSource.token); + + repo.inputBox.value = ''; + for await (const delta of response.text) { + if (tokenSource.token.isCancellationRequested) { + return null; + } + repo.inputBox.value += delta; + } + } + })); + } finally { + cancelDisposable.dispose(); + vscode.commands.executeCommand('setContext', generatingGitCommitKey, false); + } } diff --git a/extensions/positron-assistant/src/md/prompts/git/commit.md b/extensions/positron-assistant/src/md/prompts/git/commit.md new file mode 100644 index 000000000000..b05ff71ec322 --- /dev/null +++ b/extensions/positron-assistant/src/md/prompts/git/commit.md @@ -0,0 +1,3 @@ +You will be given a set of repository changes and diffs. Output a short git commit message summarizing the changes. + +Return ONLY the commit message. From 0178c2cd87071384788b4ae3807addcc0b48dc7e Mon Sep 17 00:00:00 2001 From: George Stagg Date: Tue, 24 Jun 2025 13:53:53 +0100 Subject: [PATCH 3/4] Assistant: Add experimental git integration config --- extensions/positron-assistant/package.json | 15 +++++++++++---- extensions/positron-assistant/package.nls.json | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/extensions/positron-assistant/package.json b/extensions/positron-assistant/package.json index 5cac162b25ef..d9c718a83c72 100644 --- a/extensions/positron-assistant/package.json +++ b/extensions/positron-assistant/package.json @@ -109,12 +109,12 @@ { "command": "positron-assistant.generateCommitMessage", "group": "navigation", - "when": "!positron-assistant.generatingCommitMessage" + "when": "!positron-assistant.generatingCommitMessage && config.positron.assistant.gitIntegration.enable" }, { "command": "positron-assistant.cancelGenerateCommitMessage", "group": "navigation", - "when": "positron-assistant.generatingCommitMessage" + "when": "positron-assistant.generatingCommitMessage && config.positron.assistant.gitIntegration.enable" } ] }, @@ -135,14 +135,14 @@ "command": "positron-assistant.generateCommitMessage", "title": "%commands.generateCommitMessage.title%", "category": "%commands.category%", - "enablement": "config.positron.assistant.enable", + "enablement": "config.positron.assistant.enable && config.positron.assistant.gitIntegration.enable", "icon": "$(sparkle)" }, { "command": "positron-assistant.cancelGenerateCommitMessage", "title": "%commands.cancelGenerateCommitMessage.title%", "category": "%commands.category%", - "enablement": "config.positron.assistant.enable", + "enablement": "config.positron.assistant.enable && config.positron.assistant.gitIntegration.enable", "icon": "$(stop)" } ], @@ -173,6 +173,12 @@ "items": { "type": "string" } + }, + "positron.assistant.gitIntegration.enable": { + "type": "boolean", + "default": false, + "description": "%configuration.gitIntegration.description%", + "tags": ["experimental"] } } } @@ -362,6 +368,7 @@ "canBeReferencedInPrompt": true, "userDescription": "Get changed files", "toolReferenceName": "changes", + "when": "config.positron.assistant.gitIntegration.enable", "icon": "$(diff)", "tags": [ "positron-assistant" diff --git a/extensions/positron-assistant/package.nls.json b/extensions/positron-assistant/package.nls.json index 8fe0d35e39d1..997297b57ca9 100644 --- a/extensions/positron-assistant/package.nls.json +++ b/extensions/positron-assistant/package.nls.json @@ -17,5 +17,6 @@ "configuration.enable.markdownDescription": "Enable [Positron Assistant](https://positron.posit.co/assistant), an AI assistant for Positron.", "configuration.useAnthropicSdk.description": "Use the Anthropic SDK for Anthropic models rather than the generic AI SDK.", "configuration.streamingEdits.enable": "Enable streaming edits in inline editor chats.", - "configuration.inlineCompletionExcludes.description": "A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from inline completions." + "configuration.inlineCompletionExcludes.description": "A list of [glob patterns](https://aka.ms/vscode-glob-patterns) to exclude from inline completions.", + "configuration.gitIntegration.description": "Enable Positron Assistant git integration." } From c64fb8d3545f5a41ee5a966ad96e10ceb22ebfca Mon Sep 17 00:00:00 2001 From: George Stagg Date: Wed, 25 Jun 2025 16:56:14 +0100 Subject: [PATCH 4/4] Assistant: Add tool references to prompt --- extensions/positron-assistant/src/participants.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extensions/positron-assistant/src/participants.ts b/extensions/positron-assistant/src/participants.ts index 9d3a0dc22935..51c8c55599c1 100644 --- a/extensions/positron-assistant/src/participants.ts +++ b/extensions/positron-assistant/src/participants.ts @@ -313,6 +313,16 @@ abstract class PositronAssistantParticipant implements IPositronAssistantPartici } } + // If the user has explicitly attached a tool reference, add it to the prompt. + if (request.toolReferences.length > 0) { + const referencePrompts: string[] = []; + for (const reference of request.toolReferences) { + referencePrompts.push(xml.node('tool', reference.name)); + } + const toolReferencesText = 'Attached tool references:'; + prompts.push(xml.node('tool-references', `${toolReferencesText}\n${referencePrompts.join('\n')}`)); + } + // If the user has explicitly attached files as context, add them to the prompt. if (request.references.length > 0) { const attachmentPrompts: string[] = [];