Compare commits

...

1 commit

Author SHA1 Message Date
igardev
a65ad28171 - Show error in agent view UI in case of error on editing file
- Other bug fixes
2025-10-27 02:38:06 +02:00
11 changed files with 115 additions and 67 deletions

View file

@ -51,36 +51,13 @@ export class Architect {
setOnSaveDeleteFileForDb = (context: vscode.ExtensionContext) => {
const saveListener = vscode.workspace.onDidSaveTextDocument(async (document) => {
try {
if (!this.app.configuration.rag_enabled || this.app.configuration.rag_max_files <= 0) return;
if (!this.app.chatContext.isImageOrVideoFile(document.uri.toString())){
// Update after a delay and only if the file is not changed in the meantime to avoid too often updates
let updateTime = Date.now()
let fileProperties = this.app.chatContext.getFileProperties(document.uri.toString())
if (fileProperties) fileProperties.updated = updateTime;
setTimeout(async () => {
if (fileProperties && fileProperties.updated > updateTime ) {
return;
}
this.app.chatContext.addDocument(document.uri.toString(), document.getText());
}, 5000);
}
} catch (error) {
console.error('Failed to add document to RAG:', error);
}
this.app.chatContext.udpateFileIndexing(document.uri.fsPath, document.getText());
});
context.subscriptions.push(saveListener);
// Add file delete listener for RAG
const deleteListener = vscode.workspace.onDidDeleteFiles(async (event) => {
if (!this.app.configuration.rag_enabled || this.app.configuration.rag_max_files <= 0) return;
for (const file of event.files) {
try {
await this.app.chatContext.removeDocument(file.toString());
} catch (error) {
console.error('Failed to remove document from RAG:', error);
}
}
await this.app.chatContext.removeFileIndexing(event);
});
context.subscriptions.push(deleteListener);
}
@ -461,6 +438,10 @@ export class Architect {
context.subscriptions.push(postMessageCommand);
}
private async installUpgradeLlamaCpp(isFirstStart: any) {
if (!this.app.configuration.ask_install_llamacpp) return;
let result = await Utils.executeTerminalCommand("llama-server --version");

View file

@ -275,9 +275,9 @@ export class ChatContext {
}
}
async removeDocument(uri: string) {
this.removeChunkEntries(uri);
this.filesProperties.delete(uri);
async removeDocument(filePath: string) {
this.removeChunkEntries(filePath);
this.filesProperties.delete(filePath);
}
async indexWorkspaceFiles() {
@ -408,4 +408,37 @@ export class ChatContext {
const regex = /@([a-zA-Z0-9_.-]+)(?=[,.?!\s]|$)/g;
return [...text.matchAll(regex)].map(match => match[1]);
}
public udpateFileIndexing(filePath: string, fileContent: string) {
try {
if (this.app.configuration.rag_enabled && this.app.configuration.rag_max_files > 0) {
if (!this.isImageOrVideoFile(filePath)) {
// Update after a delay and only if the file is not changed in the meantime to avoid too often updates
let updateTime = Date.now();
let fileProperties = this.getFileProperties(filePath);
if (fileProperties) fileProperties.updated = updateTime;
setTimeout(async () => {
if (fileProperties && fileProperties.updated > updateTime) {
return;
}
this.addDocument(filePath, fileContent);
}, 5000);
}
}
} catch (error) {
console.error('Failed to add/update document to RAG:', error);
}
}
public async removeFileIndexing(event: vscode.FileDeleteEvent) {
if (this.app.configuration.rag_enabled && this.app.configuration.rag_max_files > 0) {
for (const file of event.files) {
try {
await this.removeDocument(file.fsPath);
} catch (error) {
console.error('Failed to remove document from RAG:', error);
}
}
}
}
}

View file

@ -145,6 +145,8 @@ export const UI_TEXT_KEYS = {
showSelectedModels: "Show selected models",
showSelectedModelsDescription: "Displays a list of currently selected models",
useAsLocalAIRunner: "Use as local AI runner",
editMultipleFilesWithAi: "Edit multiple files with AI",
editMultipleFilesWithAiDescription: "Asks for glob pattern and prompt and edits with AI the files, which match the glob pattern with the provided prompt.",
localAIRunnerDescription: "Download models automatically from Huggingface and chat with them (as LM Studio, Ollama, etc.)",
editSettings: "Edit Settings...",
apiKeys: "API keys...",
@ -242,6 +244,9 @@ export const UI_TEXT_KEYS = {
deleteToolsModel: "Delete tools model...",
exportToolsModel: "Export tools model...",
importToolsModel: "Import tools model...",
// Other
fileUpdated: "The file is updated"
} as const;
export const PERSISTENCE_KEYS = {

View file

@ -32,7 +32,7 @@ export class FileEditor {
const prompt = await vscode.window.showInputBox({
placeHolder: 'Enter instructions for editing files...',
prompt: 'How would you like to modify the files?',
prompt: 'How would you like to modify the files? (the instructions will be applied to each file separately)',
ignoreFocusOut: true
});
if (!prompt) return;
@ -43,6 +43,12 @@ export class FileEditor {
ignoreFocusOut: true
});
if (!glob) return;
let shouldContinue = Utils.showYesNoDialog(
"You requested an edit of multiple files with AI. " +
"\n\nGlob pattern (what files to edit): " + glob +
"\nPrompt: " + prompt +
"\n\nDo you want to continue?")
if (!shouldContinue) return;
const files = await vscode.workspace.findFiles(glob);
if (!files || files.length === 0) {
@ -64,6 +70,7 @@ export class FileEditor {
vscode.window.showInformationMessage(`File editing cancelled after ${processed} of ${total} files.`);
break;
}
if (this.app.chatContext.isImageOrVideoFile(file.fsPath)) continue
progress.report({ message: `Editing ${file.fsPath}`, increment: (1 / total) * 100 });
try {
@ -80,8 +87,12 @@ export class FileEditor {
if (completion?.choices?.[0]?.message?.content) {
var edited = completion.choices[0].message.content.trim();
edited = this.removeFirstAndLastLinesIfBackticks(edited);
edited = Utils.removeFirstAndLastLinesIfBackticks(edited);
await vscode.workspace.fs.writeFile(file, Buffer.from(edited, 'utf8'));
if (this.app.configuration.rag_enabled) {
const document = await vscode.workspace.openTextDocument(file);
this.app.chatContext.udpateFileIndexing(document.uri.fsPath, document.getText())
}
}
} catch (err) {
console.error(`Failed to edit ${file.fsPath}:`, err);
@ -94,20 +105,4 @@ export class FileEditor {
}
);
}
private removeFirstAndLastLinesIfBackticks(input: string): string {
const lines = input.split('\n'); // Split the string into lines
// Remove the first line if it starts with ```
if (lines[0]?.trim().startsWith('```')) {
lines.shift(); // Remove the first line
}
// Remove the last line if it starts with ```
if (lines[lines.length - 1]?.trim().startsWith('```')) {
lines.pop(); // Remove the last line
}
return lines.join('\n'); // Join the remaining lines back into a string
}
}

View file

@ -5,6 +5,7 @@ import { Utils } from "./utils"
import { Chat } from "./types"
import { Plugin } from './plugin';
import * as fs from 'fs';
import { UI_TEXT_KEYS } from "./constants";
interface Step {
@ -263,7 +264,13 @@ export class LlamaAgent {
const toolFunc = this.app.tools.toolsFunc.get(oneToolCall.function.name);
if (toolFunc) {
commandOutput = await toolFunc(oneToolCall.function.arguments);
if (oneToolCall.function.name == "edit_file" && commandOutput != Utils.MSG_NO_UESR_PERMISSION) changedFiles.add(commandDescription);
if (oneToolCall.function.name == "edit_file" && commandOutput != Utils.MSG_NO_UESR_PERMISSION) {
changedFiles.add(commandDescription);
if (commandOutput != UI_TEXT_KEYS.fileUpdated){
this.logText += commandOutput + "\n\n"
this.app.llamaWebviewProvider.logInUi(this.logText);
}
}
if (oneToolCall.function.name == "delete_file" && commandOutput != Utils.MSG_NO_UESR_PERMISSION) deletedFiles.add(commandDescription);
}
}

View file

@ -79,6 +79,10 @@ export class Menu {
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.useAsLocalAIRunner)??"",
description: this.app.configuration.getUiText(UI_TEXT_KEYS.localAIRunnerDescription)
},
{
label: this.app.configuration.getUiText(UI_TEXT_KEYS.editMultipleFilesWithAi)??"",
description: this.app.configuration.getUiText(UI_TEXT_KEYS.editMultipleFilesWithAiDescription)
}
]
return menuItems;
@ -230,6 +234,9 @@ export class Menu {
vscode.commands.executeCommand('extension.showLlamaWebview');
this.app.llamaWebviewProvider.setView(UiView.AiRunner);
break;
case this.app.configuration.getUiText(UI_TEXT_KEYS.editMultipleFilesWithAi):
vscode.commands.executeCommand('extension.editAllSearchFiles');
break;
default:
isHandled = false;
break;

View file

@ -95,7 +95,7 @@ export class TextEditor {
vscode.window.showInformationMessage('No suggestions available');
return;
}
this.currentSuggestion = this.removeFirstAndLastLinesIfBackticks(data.choices[0].message.content.trim());
this.currentSuggestion = Utils.removeFirstAndLastLinesIfBackticks(data.choices[0].message.content.trim());
this.currentSuggestion = Utils.addLeadingSpaces(this.currentSuggestion, this.removedSpaces)
// Show the suggestion in a diff view
await this.showDiffView(editor, this.currentSuggestion);
@ -111,21 +111,7 @@ export class TextEditor {
}
}
private removeFirstAndLastLinesIfBackticks(input: string): string {
const lines = input.split('\n'); // Split the string into lines
// Remove the first line if it starts with ```
if (lines[0]?.trim().startsWith('```')) {
lines.shift(); // Remove the first line
}
// Remove the last line if it starts with ```
if (lines[lines.length - 1]?.trim().startsWith('```')) {
lines.pop(); // Remove the last line
}
return lines.join('\n'); // Join the remaining lines back into a string
}
private async showDiffView(editor: vscode.TextEditor, suggestion: string) {
// Get context before and after the selection

View file

@ -4,6 +4,7 @@ import {Utils} from "./utils";
import path from "path";
import fs from 'fs';
import { Plugin } from './plugin';
import { UI_TEXT_KEYS } from "./constants";
type ToolsMap = Map<string, (...args: any[]) => any>;
@ -215,6 +216,7 @@ export class Tools {
return `File not found at ${filePath}`;
}
fs.unlinkSync(absolutePath);
this.app.chatContext.removeDocument(absolutePath)
} catch (error) {
if (error instanceof Error) {
return `Failed to delete file at ${filePath}: ${error.message}`;
@ -252,8 +254,11 @@ export class Tools {
let changes = params.input;
if (params.input == undefined) return "The input is not provided."
let filePath = this.getFilePath(params.input);
if (!filePath) return "The file is not provided.";
try {
if (!this.app.configuration.tool_permit_file_changes){
let [yesApply, yesDontAsk] = await Utils.showYesYesdontaskNoDialog("Do you permit file " + filePath + " to be changed?")
@ -264,6 +269,9 @@ export class Tools {
if (!yesApply) return Utils.MSG_NO_UESR_PERMISSION;
}
let resultEdit = await Utils.applyEdits(changes)
if (resultEdit == UI_TEXT_KEYS.fileUpdated && this.app.configuration.rag_enabled && fs.existsSync(filePath)) {
this.app.chatContext.udpateFileIndexing(filePath, fs.readFileSync(filePath, 'utf-8'))
}
return resultEdit;
} catch (error) {
console.error('Error changes since last commit:', error);
@ -274,7 +282,7 @@ export class Tools {
public editFileDesc = async (args: string) => {
let params = JSON.parse(args);
let diffText = params.input;
if (!diffText) return "EditFile Desc - parameter input not found."
if (!diffText) return "Parameter input not found."
let filePath = this.getFilePath(diffText);
@ -751,10 +759,17 @@ export class Tools {
let blockParts = Utils.extractConflictParts("```diff" + blocks.slice(1)[0]);
filePath = blockParts[0].trim();
} else {
if (diffText.length > 0) filePath = Utils.extractConflictParts("```diff\n" + diffText)[0].trim()
if (diffText.length > 0){
if (diffText.startsWith("```\n")) diffText = diffText.slice(5)
filePath = Utils.extractConflictParts("```diff\n" + diffText)[0].trim()
}
else return "";
}
// Workaround for ClaudCode project file format - get only the relative path to the file
if (filePath.includes(" ## ")) filePath = filePath.split(" ## ")[1];
if (filePath.startsWith("## ")) filePath = filePath.slice(3);
let absolutePath = filePath;
if (!path.isAbsolute(filePath)) {
if (!vscode.workspace.workspaceFolders || vscode.workspace.workspaceFolders.length === 0) {

View file

@ -195,4 +195,6 @@ export const translations: string[][] = [
["Add tools model from OpenAI compatible provider...", "Добавяне на модел за инструменти от съвместим с OpenAI доставчик...", "Werkzeugmodell von einem mit OpenAI kompatiblen Anbieter hinzufügen...", "Добавить модель инструментов от совместимого с OpenAI поставщика...", "Añadir modelo de herramientas de un proveedor compatible con OpenAI...", "添加来自与 OpenAI 兼容提供程序的工具模型...", "Ajouter un modèle d'outils d'un fournisseur compatible OpenAI..."],
["Edit agent...", "Редактиране на агент...", "Agent bearbeiten...", "Редактировать агента...", "Editar agente...", "编辑代理...", "Modifier l'agent..."],
["Copy agent...", "Копиране на агент...", "Agent kopieren...", "Копировать агента...", "Copiar agente...", "复制代理...", "Copier l'agent..."],
["Edit multiple files with AI", "Редактиране на множество файлове с изкуствен интелект", "Mehrere Dateien mit KI bearbeiten", "Редактировать несколько файлов с ИИ", "Editar múltiples archivos con IA", "使用人工智能编辑多个文件", "Modifier plusieurs fichiers avec IA"],
["Asks for glob pattern and prompt and edits with AI the files, which match the glob pattern with the provided prompt.", "Пита за шаблон за обхват и инструкция, след което редактира с изкуствен интелект файловете, които отговарят на шаблона, със зададената инструкция.", "Fragt nach einem Glob-Muster und einer Eingabeaufforderung und bearbeitet mit KI die Dateien, die dem Glob-Muster mit der bereitgestellten Eingabeaufforderung entsprechen.", "Запрашивает шаблон glob и подсказку, а затем редактирует с помощью ИИ файлы, соответствующие шаблону, с предоставленной подсказкой.", "Pide un patrón glob y un mensaje, y edita con IA los archivos que coinciden con el patrón glob con el mensaje proporcionado.", "请求输入通配符模式和提示词,然后使用人工智能编辑符合该通配符模式的文件,并根据提供的提示词进行修改。", "Demande un motif glob et une invite, puis modifie avec IA les fichiers correspondant au motif glob avec l'invite fournie."],
];

View file

@ -8,6 +8,7 @@ import * as https from 'https';
import * as http from 'http';
import { URL } from 'url';
import { Application } from "./application";
import { UI_TEXT_KEYS } from "./constants";
interface BM25Stats {
@ -558,7 +559,7 @@ export class Utils {
static applyEdits = async (diffText: string): Promise<string> => {
// Extract edit blocks from the diff-fenced format
let ret = "The file is updated";
let ret = UI_TEXT_KEYS.fileUpdated as string;
let editBlocks: string[][] = [];
if (!diffText) return "Edit file: The input parameter is missing!";
const blocks = diffText.split("```diff")
@ -888,4 +889,20 @@ export class Utils {
vscode.window.showErrorMessage(noMsg);
}
}
static removeFirstAndLastLinesIfBackticks = (input: string): string => {
const lines = input.split('\n'); // Split the string into lines
// Remove the first line if it starts with ```
if (lines[0]?.trim().startsWith('```')) {
lines.shift(); // Remove the first line
}
// Remove the last line if it starts with ```
if (lines[lines.length - 1]?.trim().startsWith('```')) {
lines.pop(); // Remove the last line
}
return lines.join('\n'); // Join the remaining lines back into a string
}
}

View file

@ -408,7 +408,7 @@ const AgentView: React.FC<AgentViewProps> = ({
ref={textareaRef}
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="Ask me anything about your code... Press @ to select a file / for a command."
placeholder="Ask me anything about your code... Press @ to select a file, / for a command."
className="modern-textarea"
onKeyDown={(e) => {
if (e.key === 'Enter') {