Compare commits

...

6 Commits

17 changed files with 870 additions and 205 deletions

View File

@ -51,7 +51,7 @@ jobs:
}
- {
qt_version: "6.10.1",
qt_creator_version: "18.0.1"
qt_creator_version: "18.0.2"
}
steps:

View File

@ -142,7 +142,6 @@ add_qtc_plugin(QodeAssist
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp

View File

@ -20,6 +20,7 @@ qt_add_qml_module(QodeAssistChatView
qml/controls/AttachedFilesPlace.qml
qml/controls/BottomBar.qml
qml/controls/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml
qml/controls/Toast.qml
@ -68,6 +69,7 @@ qt_add_qml_module(QodeAssistChatView
FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp
FileMentionItem.hpp FileMentionItem.cpp
)
target_link_libraries(QodeAssistChatView

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -21,9 +21,12 @@
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileDialog>
#include <QFileInfo>
#include <QMessageBox>
#include <QTextStream>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h>
@ -225,6 +228,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::refreshRules);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded,
this,
&ChatRootView::openFilesChanged);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&ChatRootView::openFilesChanged);
connect(
&Settings::chatAssistantSettings().enableChatTools,
&Utils::BaseAspect::changed,
@ -738,6 +753,13 @@ void ChatRootView::openSettings()
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID);
}
void ChatRootView::openFileInEditor(const QString &filePath)
{
if (filePath.isEmpty())
return;
Core::EditorManager::openEditor(Utils::FilePath::fromString(filePath));
}
void ChatRootView::updateInputTokensCount()
{
int inputTokens = m_messageTokensCount;
@ -788,6 +810,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
if (editor) {
m_currentEditors.removeOne(editor);
}
emit openFilesChanged();
}
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
@ -805,6 +829,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
{
if (editor && editor->document()) {
m_currentEditors.append(editor);
emit openFilesChanged();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -20,6 +20,7 @@
#pragma once
#include <QQuickItem>
#include <QVariantList>
#include "ChatFileManager.hpp"
#include "ChatModel.hpp"
@ -104,6 +105,8 @@ public:
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
@ -222,6 +225,8 @@ signals:
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
void openFilesChanged();
private:
void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const;

View File

@ -0,0 +1,401 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "FileMentionItem.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::Chat {
FileMentionItem::FileMentionItem(QQuickItem *parent)
: QQuickItem(parent)
{}
QVariantList FileMentionItem::searchResults() const
{
return m_searchResults;
}
int FileMentionItem::currentIndex() const
{
return m_currentIndex;
}
void FileMentionItem::setCurrentIndex(int index)
{
if (m_currentIndex == index)
return;
m_currentIndex = index;
emit currentIndexChanged();
}
void FileMentionItem::updateSearch(const QString &query)
{
m_lastQuery = query;
QVariantList openFiles = getOpenFiles(query);
QVariantList projectResults = searchProjectFiles(query);
QSet<QString> openPaths;
for (const QVariant &item : std::as_const(openFiles)) {
const QVariantMap map = item.toMap();
openPaths.insert(map.value("absolutePath").toString());
}
QVariantList combined = openFiles;
for (const QVariant &item : std::as_const(projectResults)) {
const QVariantMap map = item.toMap();
if (!map.value("isProject").toBool()
&& openPaths.contains(map.value("absolutePath").toString()))
continue;
combined.append(item);
}
m_searchResults = combined;
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
}
void FileMentionItem::refreshSearch()
{
if (!m_lastQuery.isNull())
updateSearch(m_lastQuery);
}
void FileMentionItem::moveUp()
{
if (m_currentIndex > 0) {
m_currentIndex--;
emit currentIndexChanged();
}
}
void FileMentionItem::moveDown()
{
if (m_currentIndex < m_searchResults.size() - 1) {
m_currentIndex++;
emit currentIndexChanged();
}
}
void FileMentionItem::selectCurrent()
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
return;
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
if (item.value("isProject").toBool()) {
emit projectSelected(item.value("projectName").toString());
} else {
emit fileSelected(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString());
}
}
void FileMentionItem::dismiss()
{
m_searchResults.clear();
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
emit dismissed();
}
QVariantMap FileMentionItem::handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools)
{
QVariantMap result;
const QString fileName = relativePath.section('/', -1);
QString mentionKey = fileName;
const int colonIdx = currentQuery.indexOf(':');
if (colonIdx > 0) {
const QString projPrefix = currentQuery.left(colonIdx);
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
mentionKey = projPrefix + ":" + fileName;
}
if (useTools) {
registerMention(mentionKey, absolutePath);
result["mode"] = QStringLiteral("mention");
result["mentionText"] = "@" + mentionKey + " ";
} else {
emit fileAttachRequested({absolutePath});
result["mode"] = QStringLiteral("attach");
}
return result;
}
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
{
m_atMentionMap[mentionKey] = absolutePath;
}
void FileMentionItem::clearMentions()
{
m_atMentionMap.clear();
}
QString FileMentionItem::expandMentions(const QString &text)
{
QString result = text;
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
const QString &mentionKey = it.key();
const QString &absPath = it.value();
const QString displayName = mentionKey.section(':', -1);
const QString escaped = QRegularExpression::escape(mentionKey);
// @key:N-M -> hyperlink + inline code block
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
QList<QRegularExpressionMatch> matches;
while (matchIt.hasNext())
matches.append(matchIt.next());
for (int i = matches.size() - 1; i >= 0; --i) {
const auto &m = matches[i];
const int startLine = m.captured(1).toInt();
const int endLine = m.captured(2).toInt();
const QString ext = fileExtension(absPath);
const QString snippet = readFileLines(absPath, startLine, endLine);
const QString replacement
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
.arg(displayName)
.arg(startLine)
.arg(endLine)
.arg(absPath, ext, snippet);
result.replace(m.capturedStart(), m.capturedLength(), replacement);
}
// @key -> hyperlink only
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
}
return result;
}
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
{
QVariantList results;
struct FileResult
{
QString absolutePath;
QString relativePath;
QString projectName;
int priority;
};
const auto allProjects = ProjectExplorer::ProjectManager::projects();
QString projectFilter;
QString fileQuery = query;
const int colonIdx = query.indexOf(':');
if (colonIdx > 0) {
const QString prefix = query.left(colonIdx);
for (auto project : allProjects) {
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
projectFilter = project->displayName();
fileQuery = query.mid(colonIdx + 1);
break;
}
}
}
if (projectFilter.isEmpty() && colonIdx < 0) {
const QString lowerQ = query.toLower();
for (auto project : allProjects) {
if (!project)
continue;
const QString name = project->displayName();
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
QVariantMap item;
item["absolutePath"] = QString();
item["relativePath"] = name;
item["projectName"] = name;
item["isProject"] = true;
results.append(item);
}
}
}
QList<FileResult> candidates;
const QString lowerFileQuery = fileQuery.toLower();
const bool emptyFileQuery = fileQuery.isEmpty();
for (auto project : allProjects) {
if (!project)
continue;
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
continue;
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
const QString projectDir = project->projectDirectory().path();
const QString projectName = project->displayName();
for (const auto &filePath : projectFiles) {
const QString absolutePath = filePath.path();
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
int priority = -1;
if (emptyFileQuery) {
priority = 3;
} else if (lowerFileName == lowerFileQuery) {
priority = 0;
} else if (lowerFileName.startsWith(lowerFileQuery)) {
priority = 1;
} else if (lowerFileName.contains(lowerFileQuery)) {
priority = 2;
} else if (lowerRelativePath.contains(lowerFileQuery)) {
priority = 3;
}
if (priority >= 0)
candidates.append({absolutePath, relativePath, projectName, priority});
}
}
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
if (a.priority != b.priority)
return a.priority < b.priority;
return a.relativePath < b.relativePath;
});
const int maxFiles = qMax(0, 10 - results.size());
const int count = qMin(candidates.size(), maxFiles);
for (int i = 0; i < count; i++) {
QVariantMap item;
item["absolutePath"] = candidates[i].absolutePath;
item["relativePath"] = candidates[i].relativePath;
item["projectName"] = candidates[i].projectName;
item["isProject"] = false;
results.append(item);
}
return results;
}
QVariantList FileMentionItem::getOpenFiles(const QString &query)
{
QVariantList results;
const QString lowerQuery = query.toLower();
const bool emptyQuery = query.isEmpty();
QSet<QString> addedPaths;
auto tryAddDocument = [&](Core::IDocument *document) {
if (!document)
return;
const QString absolutePath = document->filePath().toFSPathString();
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
return;
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
if (fileName.isEmpty())
return;
QString relativePath = absolutePath;
QString projectName;
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
if (project) {
projectName = project->displayName();
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
}
if (!emptyQuery) {
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
return;
}
addedPaths.insert(absolutePath);
QVariantMap item;
item["absolutePath"] = absolutePath;
item["relativePath"] = relativePath;
item["projectName"] = projectName;
item["isProject"] = false;
item["isOpen"] = true;
results.append(item);
};
if (auto current = Core::EditorManager::currentEditor())
tryAddDocument(current->document());
for (auto editor : Core::EditorManager::visibleEditors())
if (editor)
tryAddDocument(editor->document());
for (auto document : Core::DocumentModel::openedDocuments())
tryAddDocument(document);
return results;
}
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
QTextStream stream(&file);
QString result;
int lineNum = 1;
while (!stream.atEnd()) {
const QString line = stream.readLine();
if (lineNum >= startLine)
result += line + '\n';
if (lineNum >= endLine)
break;
++lineNum;
}
return result;
}
QString FileMentionItem::fileExtension(const QString &filePath)
{
const int dot = filePath.lastIndexOf('.');
return dot >= 0 ? filePath.mid(dot + 1) : QString();
}
} // namespace QodeAssist::Chat

View File

@ -0,0 +1,84 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QHash>
#include <QQuickItem>
#include <QRegularExpression>
#include <QVariantList>
namespace QodeAssist::Chat {
class FileMentionItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
QML_ELEMENT
public:
explicit FileMentionItem(QQuickItem *parent = nullptr);
QVariantList searchResults() const;
int currentIndex() const;
void setCurrentIndex(int index);
Q_INVOKABLE void updateSearch(const QString &query);
Q_INVOKABLE void refreshSearch();
Q_INVOKABLE void moveUp();
Q_INVOKABLE void moveDown();
Q_INVOKABLE void selectCurrent();
Q_INVOKABLE void dismiss();
Q_INVOKABLE QVariantMap handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools);
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
Q_INVOKABLE void clearMentions();
Q_INVOKABLE QString expandMentions(const QString &text);
signals:
void searchResultsChanged();
void currentIndexChanged();
void fileSelected(const QString &absolutePath,
const QString &relativePath,
const QString &projectName);
void projectSelected(const QString &projectName);
void dismissed();
void fileAttachRequested(const QStringList &filePaths);
private:
QVariantList searchProjectFiles(const QString &query);
QVariantList getOpenFiles(const QString &query);
QString readFileLines(const QString &filePath, int startLine, int endLine);
static QString fileExtension(const QString &filePath);
QVariantList m_searchResults;
int m_currentIndex = 0;
QString m_lastQuery;
QHash<QString, QString> m_atMentionMap;
};
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -280,6 +280,10 @@ ChatRootView {
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx)
}
onOpenFileRequested: function(filePath) {
root.openFileInEditor(filePath)
}
}
}
@ -368,7 +372,38 @@ ChatRootView {
}
}
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
onTextChanged: {
root.calculateMessageTokensCount(messageInput.text)
var cursorPos = messageInput.cursorPosition
var textBefore = messageInput.text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
if (atIndex >= 0) {
var query = textBefore.substring(atIndex + 1)
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
fileMention.updateSearch(query)
return
}
}
fileMention.dismiss()
}
Keys.onPressed: function(event) {
if (fileMentionPopup.visible) {
if (event.key === Qt.Key_Down) {
fileMention.moveDown()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
fileMention.moveUp()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
fileMention.selectCurrent()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
fileMention.dismiss()
event.accepted = true
}
}
}
MouseArea {
anchors.fill: parent
@ -480,7 +515,7 @@ ChatRootView {
sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut
onActivated: {
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
if (messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible) {
root.sendChatMessage()
}
}
@ -497,8 +532,9 @@ ChatRootView {
}
function sendChatMessage() {
root.sendMessage(messageInput.text)
root.sendMessage(fileMention.expandMentions(messageInput.text))
messageInput.text = ""
fileMention.clearMentions()
scrollToBottom()
}
@ -572,6 +608,93 @@ ChatRootView {
infoToast.show(root.lastInfoMessage)
}
}
function onOpenFilesChanged() {
if (fileMentionPopup.visible)
Qt.callLater(fileMention.refreshSearch)
}
}
FileMentionItem {
id: fileMention
onProjectSelected: function(projectName) {
var cursorPos = messageInput.cursorPosition
var text = messageInput.text
var textBefore = text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
var mention = '@' + projectName + ':'
if (atIndex >= 0) {
var newText = text.substring(0, atIndex) + mention + text.substring(cursorPos)
messageInput.text = newText
messageInput.cursorPosition = atIndex + mention.length
}
fileMention.dismiss()
}
onFileSelected: function(absolutePath, relativePath, projectName) {
var cursorPos = messageInput.cursorPosition
var text = messageInput.text
var textBefore = text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
var currentQuery = atIndex >= 0 ? textBefore.substring(atIndex + 1) : ""
var result = fileMention.handleFileSelection(
absolutePath, relativePath, projectName, currentQuery, root.useTools)
if (result.mode === "mention") {
if (atIndex >= 0) {
let newText = text.substring(0, atIndex) + result.mentionText + text.substring(cursorPos)
messageInput.text = newText
messageInput.cursorPosition = atIndex + result.mentionText.length
}
} else {
if (atIndex >= 0) {
let newText = text.substring(0, atIndex) + text.substring(cursorPos)
messageInput.text = newText
messageInput.cursorPosition = atIndex
}
}
fileMention.dismiss()
}
onFileAttachRequested: function(filePaths) {
root.addFilesToAttachList(filePaths)
}
}
FileMentionPopup {
id: fileMentionPopup
z: 999
width: Math.min(480, root.width - 20)
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
y: view.y - height - 4
searchResults: fileMention.searchResults
onFileSelected: function(absolutePath, relativePath, projectName) {
fileMention.fileSelected(absolutePath, relativePath, projectName)
}
onProjectSelected: function(projectName) {
fileMention.projectSelected(projectName)
}
onDismissed: fileMention.dismiss()
}
Connections {
target: fileMention
function onCurrentIndexChanged() {
fileMentionPopup.currentIndex = fileMention.currentIndex
}
}
Connections {
target: fileMentionPopup
function onCurrentIndexChanged() {
fileMention.currentIndex = fileMentionPopup.currentIndex
}
}
Component.onCompleted: {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -51,6 +51,7 @@ Rectangle {
property int messageIndex: -1
signal resetChatToMessage(int index)
signal openFileRequested(string filePath)
height: msgColumn.implicitHeight + 10
radius: 8
@ -204,6 +205,15 @@ Rectangle {
}
}
onLinkActivated: function(link) {
if (link.startsWith("file://")) {
var filePath = link.replace(/^file:\/\//, "")
root.openFileRequested(filePath)
} else {
Qt.openUrlExternally(link)
}
}
ChatUtils {
id: utils
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -29,8 +29,6 @@ TextEdit {
selectionColor: palette.highlight
color: palette.text
onLinkActivated: (link) => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton

View File

@ -0,0 +1,184 @@
/*
* Copyright (C) 2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Rectangle {
id: root
property var searchResults: []
property int currentIndex: 0
signal fileSelected(string absolutePath, string relativePath, string projectName)
signal projectSelected(string projectName)
signal dismissed()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 36, 36 * 6) + 2
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
readonly property bool isProject: modelData.isProject === true
readonly property bool isOpen: modelData.isOpen === true
readonly property string fileName: {
if (isProject)
return modelData.projectName
const parts = modelData.relativePath.split('/')
return parts[parts.length - 1]
}
width: listView.width
height: 36
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
Item {
Layout.preferredWidth: 18
Layout.preferredHeight: 18
Rectangle {
anchors.fill: parent
radius: 3
visible: delegateItem.isProject || delegateItem.isOpen
color: {
if (delegateItem.index === root.currentIndex)
return Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.2)
if (delegateItem.isProject)
return Qt.rgba(palette.highlight.r,
palette.highlight.g,
palette.highlight.b, 0.3)
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
}
Text {
anchors.centerIn: parent
text: delegateItem.isProject ? "P" : "O"
font.bold: true
font.pixelSize: 10
color: {
if (delegateItem.index === root.currentIndex)
return palette.highlightedText
if (delegateItem.isProject)
return palette.highlight
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
}
}
}
}
Text {
Layout.preferredWidth: 160
text: delegateItem.fileName
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: (delegateItem.isProject ? palette.highlight : palette.text)
font.bold: true
font.italic: delegateItem.isProject
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.isProject
? "→"
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
color: delegateItem.index === root.currentIndex
? (delegateItem.isProject
? palette.highlightedText
: Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7))
: palette.mid
font.pixelSize: delegateItem.isProject ? 12 : 11
elide: Text.ElideLeft
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: handleSelection(delegateItem.modelData)
onEntered: root.currentIndex = delegateItem.index
}
}
}
function handleSelection(item) {
if (item.isProject === true) {
root.projectSelected(item.projectName)
} else {
root.fileSelected(item.absolutePath, item.relativePath, item.projectName)
}
}
function selectCurrent() {
if (currentIndex >= 0 && currentIndex < searchResults.length)
handleSelection(searchResults[currentIndex])
}
function moveDown() {
if (currentIndex < searchResults.length - 1)
currentIndex++
listView.positionViewAtIndex(currentIndex, ListView.Contain)
}
function moveUp() {
if (currentIndex > 0)
currentIndex--
listView.positionViewAtIndex(currentIndex, ListView.Contain)
}
}

View File

@ -1,7 +1,7 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.9.8",
"Version" : "0.9.9",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",

View File

@ -236,7 +236,7 @@ public:
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
closeChatViewAction.addOnTriggered(this, [this] {
if (m_chatView->isVisible()) {
if (m_chatView && m_chatView->isActive() && m_chatView->isVisible()) {
m_chatView->close();
}
});

View File

@ -84,17 +84,29 @@ QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
claudeHaiku.type = type;
claudeHaiku.isPredefined = true;
AIConfiguration mistralCodestral;
mistralCodestral.id = "preset_mistral_codestral";
mistralCodestral.name = "Mistral Codestral";
mistralCodestral.provider = "Mistral AI";
mistralCodestral.model = "codestral-2501";
mistralCodestral.url = "https://api.mistral.ai";
mistralCodestral.endpointMode = "Auto";
mistralCodestral.customEndpoint = "";
mistralCodestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
mistralCodestral.type = type;
mistralCodestral.isPredefined = true;
AIConfiguration codestral;
codestral.id = "preset_codestral";
codestral.name = "Codestral";
codestral.provider = "Codestral";
codestral.model = "codestral-2501";
codestral.url = "https://codestral.mistral.ai";
codestral.endpointMode = "Auto";
codestral.customEndpoint = "";
codestral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
codestral.type = type;
codestral.isPredefined = true;
AIConfiguration mistral;
mistral.id = "preset_mistral";
mistral.name = "Mistral";
mistral.provider = "Mistral AI";
mistral.model = type == ConfigurationType::CodeCompletion ? "mistral-medium-latest" : "mistral-large-latest";
mistral.url = "https://api.mistral.ai";
mistral.endpointMode = "Auto";
mistral.customEndpoint = "";
mistral.templateName = type == ConfigurationType::CodeCompletion ? "Mistral AI FIM" : "Mistral AI Chat";
mistral.type = type;
mistral.isPredefined = true;
AIConfiguration geminiFlash;
geminiFlash.id = "preset_gemini_flash";
@ -124,7 +136,8 @@ QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
presets.append(claudeHaiku);
presets.append(claudeOpus);
presets.append(gpt52codex);
presets.append(mistralCodestral);
presets.append(codestral);
presets.append(mistral);
presets.append(geminiFlash);
return presets;

View File

@ -1,132 +0,0 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ReadVisibleFilesTool.hpp"
#include "ToolExceptions.hpp"
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <logger/Logger.hpp>
#include <projectexplorer/projectmanager.h>
#include <QJsonArray>
#include <QJsonObject>
#include <QtConcurrent>
namespace QodeAssist::Tools {
ReadVisibleFilesTool::ReadVisibleFilesTool(QObject *parent)
: BaseTool(parent)
, m_ignoreManager(new Context::IgnoreManager(this))
{}
QString ReadVisibleFilesTool::name() const
{
return "read_visible_files";
}
QString ReadVisibleFilesTool::stringName() const
{
return {"Reading currently opened and visible files in IDE editors"};
}
QString ReadVisibleFilesTool::description() const
{
return "Read content from all currently visible editor tabs, including unsaved changes. "
"Returns file paths and content. No parameters required.";
}
QJsonObject ReadVisibleFilesTool::getDefinition(LLMCore::ToolSchemaFormat format) const
{
QJsonObject definition;
definition["type"] = "object";
definition["properties"] = QJsonObject();
definition["required"] = QJsonArray();
switch (format) {
case LLMCore::ToolSchemaFormat::OpenAI:
return customizeForOpenAI(definition);
case LLMCore::ToolSchemaFormat::Claude:
return customizeForClaude(definition);
case LLMCore::ToolSchemaFormat::Ollama:
return customizeForOllama(definition);
case LLMCore::ToolSchemaFormat::Google:
return customizeForGoogle(definition);
}
return definition;
}
LLMCore::ToolPermissions ReadVisibleFilesTool::requiredPermissions() const
{
return LLMCore::ToolPermission::FileSystemRead;
}
QFuture<QString> ReadVisibleFilesTool::executeAsync(const QJsonObject &input)
{
Q_UNUSED(input)
return QtConcurrent::run([this]() -> QString {
auto editors = Core::EditorManager::visibleEditors();
if (editors.isEmpty()) {
QString error = "Error: No visible files in the editor";
throw ToolRuntimeError(error);
}
QStringList results;
for (auto editor : editors) {
if (!editor || !editor->document()) {
continue;
}
QString filePath = editor->document()->filePath().toFSPathString();
auto project = ProjectExplorer::ProjectManager::projectForFile(
editor->document()->filePath());
if (project && m_ignoreManager->shouldIgnore(filePath, project)) {
LOG_MESSAGE(
QString("Ignoring visible file due to .qodeassistignore: %1").arg(filePath));
continue;
}
QByteArray contentBytes = editor->document()->contents();
QString fileContent = QString::fromUtf8(contentBytes);
QString fileResult;
if (fileContent.isEmpty()) {
fileResult
= QString("File: %1\n\nThe file is empty or could not be read").arg(filePath);
} else {
fileResult = QString("File: %1\n\nContent:\n%2").arg(filePath, fileContent);
}
results.append(fileResult);
}
if (results.isEmpty()) {
QString error = "Error: All visible files are excluded by .qodeassistignore";
throw ToolRuntimeError(error);
}
return results.join("\n\n" + QString(80, '=') + "\n\n");
});
}
} // namespace QodeAssist::Tools

View File

@ -1,45 +0,0 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <context/IgnoreManager.hpp>
#include <llmcore/BaseTool.hpp>
namespace QodeAssist::Tools {
class ReadVisibleFilesTool : public LLMCore::BaseTool
{
Q_OBJECT
public:
explicit ReadVisibleFilesTool(QObject *parent = nullptr);
QString name() const override;
QString stringName() const override;
QString description() const override;
QJsonObject getDefinition(LLMCore::ToolSchemaFormat format) const override;
LLMCore::ToolPermissions requiredPermissions() const override;
QFuture<QString> executeAsync(const QJsonObject &input = QJsonObject()) override;
private:
Context::IgnoreManager *m_ignoreManager;
};
} // namespace QodeAssist::Tools

View File

@ -33,7 +33,6 @@
#include "GetIssuesListTool.hpp"
#include "ListProjectFilesTool.hpp"
#include "ProjectSearchTool.hpp"
#include "ReadVisibleFilesTool.hpp"
#include "TodoTool.hpp"
namespace QodeAssist::Tools {
@ -46,7 +45,6 @@ ToolsFactory::ToolsFactory(QObject *parent)
void ToolsFactory::registerTools()
{
registerTool(new ReadVisibleFilesTool(this));
registerTool(new ListProjectFilesTool(this));
registerTool(new GetIssuesListTool(this));
registerTool(new CreateNewFileTool(this));