Add a Git section to the context mentions

This commit is contained in:
Matt Rubens
2025-01-06 01:50:06 -05:00
parent 6e834d2fc3
commit 7e9ea7ac28
17 changed files with 987 additions and 207 deletions

View File

@@ -12,7 +12,7 @@ import { ApiHandler, SingleCompletionHandler, buildApiHandler } from "../api"
import { ApiStream } from "../api/transform/stream"
import { DiffViewProvider } from "../integrations/editor/DiffViewProvider"
import { findToolName, formatContentBlockToMarkdown } from "../integrations/misc/export-markdown"
import { extractTextFromFile, addLineNumbers, stripLineNumbers, everyLineHasLineNumbers } from "../integrations/misc/extract-text"
import { extractTextFromFile, addLineNumbers, stripLineNumbers, everyLineHasLineNumbers, truncateOutput } from "../integrations/misc/extract-text"
import { TerminalManager } from "../integrations/terminal/TerminalManager"
import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
import { listFiles } from "../services/glob/list-files"
@@ -716,22 +716,6 @@ export class Cline {
}
})
const getFormattedOutput = async () => {
const { terminalOutputLineLimit } = await this.providerRef.deref()?.getState() ?? {}
const limit = terminalOutputLineLimit ?? 0
if (limit > 0 && lines.length > limit) {
const beforeLimit = Math.floor(limit * 0.2) // 20% of lines before
const afterLimit = limit - beforeLimit // remaining 80% after
return [
...lines.slice(0, beforeLimit),
`\n[...${lines.length - limit} lines omitted...]\n`,
...lines.slice(-afterLimit)
].join('\n')
}
return lines.join('\n')
}
let completed = false
process.once("completed", () => {
completed = true
@@ -750,7 +734,8 @@ export class Cline {
// grouping command_output messages despite any gaps anyways)
await delay(50)
const output = await getFormattedOutput()
const { terminalOutputLineLimit } = await this.providerRef.deref()?.getState() ?? {}
const output = truncateOutput(lines.join('\n'), terminalOutputLineLimit)
const result = output.trim()
if (userFeedback) {

View File

@@ -0,0 +1,155 @@
// Create mock vscode module before importing anything
const createMockUri = (scheme: string, path: string) => ({
scheme,
authority: '',
path,
query: '',
fragment: '',
fsPath: path,
with: jest.fn(),
toString: () => path,
toJSON: () => ({
scheme,
authority: '',
path,
query: '',
fragment: ''
})
})
const mockExecuteCommand = jest.fn()
const mockOpenExternal = jest.fn()
const mockShowErrorMessage = jest.fn()
const mockVscode = {
workspace: {
workspaceFolders: [{
uri: { fsPath: "/test/workspace" }
}]
},
window: {
showErrorMessage: mockShowErrorMessage,
showInformationMessage: jest.fn(),
showWarningMessage: jest.fn(),
createTextEditorDecorationType: jest.fn(),
createOutputChannel: jest.fn(),
createWebviewPanel: jest.fn(),
activeTextEditor: undefined
},
commands: {
executeCommand: mockExecuteCommand
},
env: {
openExternal: mockOpenExternal
},
Uri: {
parse: jest.fn((url: string) => createMockUri('https', url)),
file: jest.fn((path: string) => createMockUri('file', path))
},
Position: jest.fn(),
Range: jest.fn(),
TextEdit: jest.fn(),
WorkspaceEdit: jest.fn(),
DiagnosticSeverity: {
Error: 0,
Warning: 1,
Information: 2,
Hint: 3
}
}
// Mock modules
jest.mock('vscode', () => mockVscode)
jest.mock("../../../services/browser/UrlContentFetcher")
jest.mock("../../../utils/git")
// Now import the modules that use the mocks
import { parseMentions, openMention } from "../index"
import { UrlContentFetcher } from "../../../services/browser/UrlContentFetcher"
import * as git from "../../../utils/git"
describe("mentions", () => {
const mockCwd = "/test/workspace"
let mockUrlContentFetcher: UrlContentFetcher
beforeEach(() => {
jest.clearAllMocks()
// Create a mock instance with just the methods we need
mockUrlContentFetcher = {
launchBrowser: jest.fn().mockResolvedValue(undefined),
closeBrowser: jest.fn().mockResolvedValue(undefined),
urlToMarkdown: jest.fn().mockResolvedValue(""),
} as unknown as UrlContentFetcher
})
describe("parseMentions", () => {
it("should parse git commit mentions", async () => {
const commitHash = "abc1234"
const commitInfo = `abc1234 Fix bug in parser
Author: John Doe
Date: Mon Jan 5 23:50:06 2025 -0500
Detailed commit message with multiple lines
- Fixed parsing issue
- Added tests`
jest.mocked(git.getCommitInfo).mockResolvedValue(commitInfo)
const result = await parseMentions(
`Check out this commit @${commitHash}`,
mockCwd,
mockUrlContentFetcher
)
expect(result).toContain(`'${commitHash}' (see below for commit info)`)
expect(result).toContain(`<git_commit hash="${commitHash}">`)
expect(result).toContain(commitInfo)
})
it("should handle errors fetching git info", async () => {
const commitHash = "abc1234"
const errorMessage = "Failed to get commit info"
jest.mocked(git.getCommitInfo).mockRejectedValue(new Error(errorMessage))
const result = await parseMentions(
`Check out this commit @${commitHash}`,
mockCwd,
mockUrlContentFetcher
)
expect(result).toContain(`'${commitHash}' (see below for commit info)`)
expect(result).toContain(`<git_commit hash="${commitHash}">`)
expect(result).toContain(`Error fetching commit info: ${errorMessage}`)
})
})
describe("openMention", () => {
it("should handle file paths and problems", async () => {
await openMention("/path/to/file")
expect(mockExecuteCommand).not.toHaveBeenCalled()
expect(mockOpenExternal).not.toHaveBeenCalled()
expect(mockShowErrorMessage).toHaveBeenCalledWith("Could not open file!")
await openMention("problems")
expect(mockExecuteCommand).toHaveBeenCalledWith("workbench.actions.view.problems")
})
it("should handle URLs", async () => {
const url = "https://example.com"
await openMention(url)
const mockUri = mockVscode.Uri.parse(url)
expect(mockOpenExternal).toHaveBeenCalled()
const calledArg = mockOpenExternal.mock.calls[0][0]
expect(calledArg).toEqual(expect.objectContaining({
scheme: mockUri.scheme,
authority: mockUri.authority,
path: mockUri.path,
query: mockUri.query,
fragment: mockUri.fragment
}))
})
})
})

View File

@@ -2,27 +2,28 @@ import * as vscode from "vscode"
import * as path from "path"
import { openFile } from "../../integrations/misc/open-file"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { mentionRegexGlobal } from "../../shared/context-mentions"
import { mentionRegexGlobal, formatGitSuggestion, type MentionSuggestion } from "../../shared/context-mentions"
import fs from "fs/promises"
import { extractTextFromFile } from "../../integrations/misc/extract-text"
import { isBinaryFile } from "isbinaryfile"
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"
import { getCommitInfo, getWorkingState } from "../../utils/git"
export function openMention(mention?: string): void {
export async function openMention(mention?: string): Promise<void> {
if (!mention) {
return
}
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return
}
if (mention.startsWith("/")) {
const relPath = mention.slice(1)
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (!cwd) {
return
}
const absPath = path.resolve(cwd, relPath)
if (mention.endsWith("/")) {
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
// vscode.commands.executeCommand("vscode.openFolder", , { forceNewWindow: false }) opens in new window
} else {
openFile(absPath)
}
@@ -40,12 +41,16 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
if (mention.startsWith("http")) {
return `'${mention}' (see below for site content)`
} else if (mention.startsWith("/")) {
const mentionPath = mention.slice(1) // Remove the leading '/'
const mentionPath = mention.slice(1)
return mentionPath.endsWith("/")
? `'${mentionPath}' (see below for folder content)`
: `'${mentionPath}' (see below for file content)`
} else if (mention === "problems") {
return `Workspace Problems (see below for diagnostics)`
} else if (mention === "git-changes") {
return `Working directory changes (see below for details)`
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
return `Git commit '${mention}' (see below for commit info)`
}
return match
})
@@ -99,6 +104,20 @@ export async function parseMentions(text: string, cwd: string, urlContentFetcher
} catch (error) {
parsedText += `\n\n<workspace_diagnostics>\nError fetching diagnostics: ${error.message}\n</workspace_diagnostics>`
}
} else if (mention === "git-changes") {
try {
const workingState = await getWorkingState(cwd)
parsedText += `\n\n<git_working_state>\n${workingState}\n</git_working_state>`
} catch (error) {
parsedText += `\n\n<git_working_state>\nError fetching working state: ${error.message}\n</git_working_state>`
}
} else if (/^[a-f0-9]{7,40}$/.test(mention)) {
try {
const commitInfo = await getCommitInfo(mention, cwd)
parsedText += `\n\n<git_commit hash="${mention}">\n${commitInfo}\n</git_commit>`
} catch (error) {
parsedText += `\n\n<git_commit hash="${mention}">\nError fetching commit info: ${error.message}\n</git_commit>`
}
}
}
@@ -137,7 +156,6 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
folderContent += `${linePrefix}${entry.name}\n`
const filePath = path.join(mentionPath, entry.name)
const absoluteFilePath = path.resolve(absPath, entry.name)
// const relativeFilePath = path.relative(cwd, absoluteFilePath);
fileContentPromises.push(
(async () => {
try {
@@ -154,7 +172,6 @@ async function getFileOrFolderContent(mentionPath: string, cwd: string): Promise
)
} else if (entry.isDirectory()) {
folderContent += `${linePrefix}${entry.name}/\n`
// not recursively getting folder contents
} else {
folderContent += `${linePrefix}${entry.name}\n`
}

View File

@@ -24,6 +24,7 @@ import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
import { enhancePrompt } from "../../utils/enhance-prompt"
import { getCommitInfo, searchCommits, getWorkingState } from "../../utils/git"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -732,6 +733,24 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
}
break
case "searchCommits": {
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
if (cwd) {
try {
const commits = await searchCommits(message.query || "", cwd)
await this.postMessageToWebview({
type: "commitSearchResults",
commits
})
} catch (error) {
console.error("Error searching commits:", error)
vscode.window.showErrorMessage("Failed to search commits")
}
}
break
}
}
},
null,