Compare commits

..

16 Commits

Author SHA1 Message Date
1d64f94446 refactor: Remove current visible files in ide 2026-02-19 18:31:58 +01:00
356015a5b5 feat: Add file search to chat
feat: Add opened documents files
2026-02-19 18:29:30 +01:00
ec45067336 chore: Upgrade plugin to 0.9.9 version 2026-01-27 22:41:57 +01:00
52fb65c5b1 feat: Add support QtCreator 18.0.2 2026-01-27 22:41:20 +01:00
478f369ad2 feat: Add codestral and mistral quick setup 2026-01-27 22:41:02 +01:00
762c965377 fix: Add preconditions for windows chat 2026-01-27 22:35:02 +01:00
d2b93310e2 chore: Update plugin to 0.9.8 2026-01-20 20:00:49 +01:00
f3b1e7f411 Add quick setup screenshot 2026-01-20 19:57:44 +01:00
a55c6ccfdb feat: Add predefined templates 2026-01-20 19:54:16 +01:00
b32433c336 refactor: Change quick refactor ui layout 2026-01-20 18:08:49 +01:00
6f11260cd1 refactor: Change UI for fix behavior 2026-01-19 23:52:44 +01:00
ddd6aba091 fix: Remove close chat action from editor context menu 2026-01-19 23:17:31 +01:00
e3f464c54e fix: Create _content folder only when there is an attachment (#297) 2025-12-16 13:19:10 +01:00
e86e58337a Update QodeAssist version range for Qt Creator 16.0.2 2025-12-15 01:00:00 +01:00
dbd47387be chore: Update plugin to 0.9.7 2025-12-15 00:47:50 +01:00
50e1276ab2 feat: Add support QtC 18.0.1 (#296)
* feat: Add support QtC 18.0.1
* feat: Remove support QtC 16.0.2
2025-12-14 02:53:58 +01:00
24 changed files with 1368 additions and 373 deletions

View File

@ -45,17 +45,13 @@ jobs:
cc: "clang", cxx: "clang++"
}
qt_config:
- {
qt_version: "6.8.3",
qt_creator_version: "16.0.2"
}
- {
qt_version: "6.9.2",
qt_creator_version: "17.0.2"
}
- {
qt_version: "6.10.0",
qt_creator_version: "18.0.0"
qt_version: "6.10.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

@ -38,14 +38,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, "Failed to create directory structure"};
}
QString contentFolder = getChatContentFolder(filePath);
QDir dir;
if (!dir.exists(contentFolder)) {
if (!dir.mkpath(contentFolder)) {
LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder));
}
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
@ -88,21 +80,22 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
@ -113,7 +106,7 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
@ -125,11 +118,12 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
}
messageObj["images"] = imagesArray;
}
return messageObj;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
ChatModel::Message ChatSerializer::deserializeMessage(
const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
@ -137,7 +131,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
if (json.contains("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
@ -148,7 +142,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
@ -160,7 +154,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
message.images.append(image);
}
}
return message;
}
@ -178,7 +172,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
return root;
}
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
bool ChatSerializer::deserializeChat(
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
{
QJsonArray messagesArray = json["messages"].toArray();
QVector<ChatModel::Message> messages;
@ -189,17 +184,24 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
}
model->clear();
model->setLoadingFromHistory(true);
for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id, message.attachments, message.images, message.isRedacted, message.signature);
model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size())
.arg(message.isRedacted)
.arg(message.signature.length()));
}
model->setLoadingFromHistory(false);
return true;
@ -217,12 +219,14 @@ bool ChatSerializer::validateVersion(const QString &version)
if (version == VERSION) {
return true;
}
if (version == "0.1") {
LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content");
LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true;
}
return false;
}
@ -234,10 +238,11 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
return QDir(dirPath).filePath(baseName + "_content");
}
bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
bool ChatSerializer::saveContentToStorage(
const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
{
QString contentFolder = getChatContentFolder(chatFilePath);
QDir dir;
@ -247,34 +252,34 @@ bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
return false;
}
}
QFileInfo originalFileInfo(fileName);
QString extension = originalFileInfo.suffix();
QString baseName = originalFileInfo.completeBaseName();
QString uniqueName = QString("%1_%2.%3")
.arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension);
.arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension);
QString fullPath = QDir(contentFolder).filePath(uniqueName);
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
return false;
}
if (file.write(contentData) == -1) {
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
return false;
}
file.close();
storedPath = uniqueName;
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
return true;
}
@ -282,16 +287,16 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
{
QString contentFolder = getChatContentFolder(chatFilePath);
QString fullPath = QDir(contentFolder).filePath(storedPath);
QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
return QString();
}
QByteArray contentData = file.readAll();
file.close();
return contentData.toBase64();
}

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.6",
"Version" : "0.9.9",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",

View File

@ -125,16 +125,34 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
### Quick Setup (Recommended for Beginners)
### Supported Providers
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
<details>
<summary>Quick setup: (click to expand)</summary>
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
</details>
1. **Open QodeAssist Settings**
2. **Select a Preset** - Choose from the Quick Setup dropdown:
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
- **OpenAI** (gpt-5.2-codex)
- **Mistral AI** (Codestral 2501)
- **Google AI** (Gemini 2.5 Flash)
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
### Manual Provider Configuration
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
@ -360,7 +378,7 @@ See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](do
| Qt Creator Version | QodeAssist Version |
|-------------------|-------------------|
| 17.0.0+ | 0.6.0 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.9.6 |
| 16.0.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 |

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();
}
});
@ -250,8 +250,6 @@ public:
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(showChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(closeChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE);
}
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();

View File

@ -43,6 +43,106 @@ ConfigurationManager &ConfigurationManager::instance()
return instance;
}
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
ConfigurationType type)
{
QVector<AIConfiguration> presets;
AIConfiguration claudeOpus;
claudeOpus.id = "preset_claude_opus";
claudeOpus.name = "Claude Opus 4.5";
claudeOpus.provider = "Claude";
claudeOpus.model = "claude-opus-4-5-20251101";
claudeOpus.url = "https://api.anthropic.com";
claudeOpus.endpointMode = "Auto";
claudeOpus.customEndpoint = "";
claudeOpus.templateName = "Claude";
claudeOpus.type = type;
claudeOpus.isPredefined = true;
AIConfiguration claudeSonnet;
claudeSonnet.id = "preset_claude_sonnet";
claudeSonnet.name = "Claude Sonnet 4.5";
claudeSonnet.provider = "Claude";
claudeSonnet.model = "claude-sonnet-4-5-20250929";
claudeSonnet.url = "https://api.anthropic.com";
claudeSonnet.endpointMode = "Auto";
claudeSonnet.customEndpoint = "";
claudeSonnet.templateName = "Claude";
claudeSonnet.type = type;
claudeSonnet.isPredefined = true;
AIConfiguration claudeHaiku;
claudeHaiku.id = "preset_claude_haiku";
claudeHaiku.name = "Claude Haiku 4.5";
claudeHaiku.provider = "Claude";
claudeHaiku.model = "claude-haiku-4-5-20251001";
claudeHaiku.url = "https://api.anthropic.com";
claudeHaiku.endpointMode = "Auto";
claudeHaiku.customEndpoint = "";
claudeHaiku.templateName = "Claude";
claudeHaiku.type = type;
claudeHaiku.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";
geminiFlash.name = "Gemini 2.5 Flash";
geminiFlash.provider = "Google AI";
geminiFlash.model = "gemini-2.5-flash";
geminiFlash.url = "https://generativelanguage.googleapis.com/v1beta";
geminiFlash.endpointMode = "Auto";
geminiFlash.customEndpoint = "";
geminiFlash.templateName = "Google AI";
geminiFlash.type = type;
geminiFlash.isPredefined = true;
AIConfiguration gpt52codex;
gpt52codex.id = "preset_gpt52codex";
gpt52codex.name = "gpt-5.2-codex";
gpt52codex.provider = "OpenAI Responses";
gpt52codex.model = "gpt-5.2-codex";
gpt52codex.url = "https://api.openai.com";
gpt52codex.endpointMode = "Auto";
gpt52codex.customEndpoint = "";
gpt52codex.templateName = "OpenAI Responses";
gpt52codex.type = type;
gpt52codex.isPredefined = true;
presets.append(claudeSonnet);
presets.append(claudeHaiku);
presets.append(claudeOpus);
presets.append(gpt52codex);
presets.append(codestral);
presets.append(mistral);
presets.append(geminiFlash);
return presets;
}
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
{
switch (type) {
@ -94,6 +194,9 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
configs->clear();
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
configs->append(predefinedConfigs);
if (!ensureDirectoryExists(type)) {
LOG_MESSAGE("Failed to create configuration directory");
return false;
@ -131,6 +234,7 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
config.customEndpoint = obj["customEndpoint"].toString();
config.type = type;
config.formatVersion = obj.value("formatVersion").toInt(1);
config.isPredefined = false;
if (config.id.isEmpty() || config.name.isEmpty()) {
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
@ -185,6 +289,12 @@ bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
{
AIConfiguration config = getConfigurationById(id, type);
if (config.isPredefined) {
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
return false;
}
QDir dir(getConfigurationDirectory(type));
QStringList filters;
filters << QString("*_%1.json").arg(id);

View File

@ -41,6 +41,7 @@ struct AIConfiguration
QString customEndpoint;
ConfigurationType type;
int formatVersion = CONFIGURATION_FORMAT_VERSION;
bool isPredefined = false;
};
class ConfigurationManager : public QObject
@ -58,6 +59,8 @@ public:
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
QString getConfigurationDirectory(ConfigurationType type) const;
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
signals:
void configurationsChanged(ConfigurationType type);

View File

@ -23,6 +23,7 @@
#include <coreplugin/icore.h>
#include <utils/layoutbuilder.h>
#include <utils/utilsicons.h>
#include <QComboBox>
#include <QDesktopServices>
#include <QDir>
#include <QInputDialog>
@ -88,6 +89,27 @@ GeneralSettings::GeneralSettings()
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
ccPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
ccPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(ccPresetConfig, ConfigurationType::CodeCompletion);
ccConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
ccConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
caPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
caPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(caPresetConfig, ConfigurationType::Chat);
caConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
caConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
qrPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
qrPresetConfig.setLabelText(Tr::tr("Quick Setup"));
loadPresetConfigurations(qrPresetConfig, ConfigurationType::QuickRefactor);
qrConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
qrConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true);
@ -127,6 +149,7 @@ GeneralSettings::GeneralSettings()
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
ccLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
ccOpenConfigFolder.m_isCompact = true;
@ -218,6 +241,7 @@ GeneralSettings::GeneralSettings()
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
caLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
caOpenConfigFolder.m_isCompact = true;
@ -262,6 +286,7 @@ GeneralSettings::GeneralSettings()
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
qrLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
qrOpenConfigFolder.m_isCompact = true;
@ -325,17 +350,24 @@ GeneralSettings::GeneralSettings()
title(TrConstants::CODE_COMPLETION),
Column{
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}},
ccGrid,
Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}};
auto caGroup = Group{
title(TrConstants::CHAT_ASSISTANT),
Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}};
Column{
Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}},
Row{caPresetConfig, caConfigureApiKey, Stretch{1}},
caGrid}};
auto qrGroup = Group{
title(TrConstants::QUICK_REFACTOR),
Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}};
Column{
Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}},
Row{qrPresetConfig, qrConfigureApiKey, Stretch{1}},
qrGrid}};
auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
@ -570,6 +602,33 @@ void GeneralSettings::setupConnections()
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
});
connect(&ccPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(ccPresetConfig.volatileValue(), ConfigurationType::CodeCompletion);
ccPresetConfig.setValue(0);
});
connect(&ccConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&caPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(caPresetConfig.volatileValue(), ConfigurationType::Chat);
caPresetConfig.setValue(0);
});
connect(&caConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&qrPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
applyPresetConfiguration(qrPresetConfig.volatileValue(), ConfigurationType::QuickRefactor);
qrPresetConfig.setValue(0);
});
connect(&qrConfigureApiKey, &ButtonAspect::clicked, this, []() {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue());
@ -776,11 +835,33 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
dialog.addLabel(TrConstants::SELECT_CONFIGURATION);
int predefinedCount = 0;
for (const AIConfiguration &config : configs) {
if (config.isPredefined) {
predefinedCount++;
}
}
if (predefinedCount > 0) {
auto *hintLabel = dialog.addLabel(
Tr::tr("[Preset] configurations are predefined cloud models ready to use."));
QFont hintFont = hintLabel->font();
hintFont.setItalic(true);
hintFont.setPointSize(hintFont.pointSize() - 1);
hintLabel->setFont(hintFont);
hintLabel->setStyleSheet("color: gray;");
}
dialog.addSpacing();
QStringList configNames;
for (const AIConfiguration &config : configs) {
configNames.append(config.name);
QString displayName = config.name;
if (config.isPredefined) {
displayName = QString("[Preset] %1").arg(config.name);
}
configNames.append(displayName);
}
auto configList = dialog.addComboBox(configNames, QString());
@ -790,9 +871,31 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
auto *okButton = new QPushButton(TrConstants::OK);
auto *cancelButton = new QPushButton(TrConstants::CANCEL);
auto updateDeleteButtonState = [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
deleteButton->setEnabled(!configs[currentIndex].isPredefined);
}
};
connect(configList,
QOverload<int>::of(&QComboBox::currentIndexChanged),
updateDeleteButtonState);
updateDeleteButtonState();
connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (configToDelete.isPredefined) {
QMessageBox::information(
&dialog,
TrConstants::DELETE_CONFIGURATION,
Tr::tr("Predefined configurations cannot be deleted."));
return;
}
QMessageBox::StandardButton reply = QMessageBox::question(
&dialog,
TrConstants::DELETE_CONFIGURATION,
@ -800,7 +903,6 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (manager.deleteConfiguration(configToDelete.id, type)) {
dialog.accept();
onLoadConfiguration(prefix);
@ -860,6 +962,73 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
dialog.exec();
}
void GeneralSettings::loadPresetConfigurations(Utils::SelectionAspect &aspect,
ConfigurationType type)
{
QVector<AIConfiguration> presets = ConfigurationManager::getPredefinedConfigurations(type);
if (type == ConfigurationType::CodeCompletion) {
m_ccPresets = presets;
} else if (type == ConfigurationType::Chat) {
m_caPresets = presets;
} else if (type == ConfigurationType::QuickRefactor) {
m_qrPresets = presets;
}
aspect.addOption(Tr::tr("-- Select Preset --"));
for (const AIConfiguration &config : presets) {
aspect.addOption(config.name);
}
aspect.setDefaultValue(0);
}
void GeneralSettings::applyPresetConfiguration(int index, ConfigurationType type)
{
if (index <= 0) {
return;
}
QVector<AIConfiguration> *presets = nullptr;
if (type == ConfigurationType::CodeCompletion) {
presets = &m_ccPresets;
} else if (type == ConfigurationType::Chat) {
presets = &m_caPresets;
} else if (type == ConfigurationType::QuickRefactor) {
presets = &m_qrPresets;
}
if (!presets || index - 1 >= presets->size()) {
return;
}
const AIConfiguration &config = presets->at(index - 1);
if (type == ConfigurationType::CodeCompletion) {
ccProvider.setValue(config.provider);
ccModel.setValue(config.model);
ccTemplate.setValue(config.templateName);
ccUrl.setValue(config.url);
ccEndpointMode.setValue(ccEndpointMode.indexForDisplay(config.endpointMode));
ccCustomEndpoint.setValue(config.customEndpoint);
} else if (type == ConfigurationType::Chat) {
caProvider.setValue(config.provider);
caModel.setValue(config.model);
caTemplate.setValue(config.templateName);
caUrl.setValue(config.url);
caEndpointMode.setValue(caEndpointMode.indexForDisplay(config.endpointMode));
caCustomEndpoint.setValue(config.customEndpoint);
} else if (type == ConfigurationType::QuickRefactor) {
qrProvider.setValue(config.provider);
qrModel.setValue(config.model);
qrTemplate.setValue(config.templateName);
qrUrl.setValue(config.url);
qrEndpointMode.setValue(qrEndpointMode.indexForDisplay(config.endpointMode));
qrCustomEndpoint.setValue(config.customEndpoint);
}
writeSettings();
}
class GeneralSettingsPage : public Core::IOptionsPage
{
public:

View File

@ -23,6 +23,7 @@
#include <QPointer>
#include "ButtonAspect.hpp"
#include "ConfigurationManager.hpp"
namespace Utils {
class DetailsWidget;
@ -46,6 +47,9 @@ public:
ButtonAspect resetToDefaults{this};
// code completion setttings
Utils::SelectionAspect ccPresetConfig{this};
ButtonAspect ccConfigureApiKey{this};
Utils::StringAspect ccProvider{this};
ButtonAspect ccSelectProvider{this};
@ -91,6 +95,9 @@ public:
ButtonAspect ccPreset1SelectTemplate{this};
// chat assistant settings
Utils::SelectionAspect caPresetConfig{this};
ButtonAspect caConfigureApiKey{this};
Utils::StringAspect caProvider{this};
ButtonAspect caSelectProvider{this};
@ -116,6 +123,9 @@ public:
ButtonAspect caOpenConfigFolder{this};
// quick refactor settings
Utils::SelectionAspect qrPresetConfig{this};
ButtonAspect qrConfigureApiKey{this};
Utils::StringAspect qrProvider{this};
ButtonAspect qrSelectProvider{this};
@ -162,10 +172,17 @@ public:
void onSaveConfiguration(const QString &prefix);
void onLoadConfiguration(const QString &prefix);
void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type);
void applyPresetConfiguration(int index, ConfigurationType type);
private:
void setupConnections();
void resetPageToDefaults();
QVector<AIConfiguration> m_ccPresets;
QVector<AIConfiguration> m_caPresets;
QVector<AIConfiguration> m_qrPresets;
};
GeneralSettings &generalSettings();

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));

View File

@ -34,6 +34,7 @@
#include <QDialogButtonBox>
#include <QDir>
#include <QFontMetrics>
#include <QFrame>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
@ -72,15 +73,15 @@ static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
painter.end();
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
uchar *bits = image.bits();
const int bytesPerPixel = 4;
const int totalBytes = image.width() * image.height() * bytesPerPixel;
const int newR = color.red();
const int newG = color.green();
const int newB = color.blue();
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
int alpha = bits[i + 3];
if (alpha > 0) {
@ -100,11 +101,17 @@ QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastIns
setWindowTitle(Tr::tr("Quick Refactor"));
setupUi();
if (!m_lastInstructions.isEmpty()) {
m_instructionEdit->setPlainText(m_lastInstructions);
m_instructionEdit->selectAll();
}
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
m_textEdit->installEventFilter(this);
m_instructionEdit->installEventFilter(this);
m_commandsComboBox->installEventFilter(this);
updateDialogSize();
m_commandsComboBox->setFocus();
m_instructionEdit->setFocus();
}
void QuickRefactorDialog::setupUi()
@ -173,56 +180,65 @@ void QuickRefactorDialog::setupUi()
mainLayout->addLayout(actionsLayout);
QHBoxLayout *instructionsLayout = new QHBoxLayout();
instructionsLayout->setSpacing(4);
QLabel *instructionLabel = new QLabel(Tr::tr("Your Current Instruction:"), this);
mainLayout->addWidget(instructionLabel);
QLabel *instructionsLabel = new QLabel(Tr::tr("Custom Instructions:"), this);
instructionsLayout->addWidget(instructionsLabel);
m_instructionEdit = new QPlainTextEdit(this);
m_instructionEdit->setMinimumHeight(80);
m_instructionEdit->setPlaceholderText(Tr::tr("Type or edit your instruction..."));
mainLayout->addWidget(m_instructionEdit);
QHBoxLayout *savedInstructionsLayout = new QHBoxLayout();
savedInstructionsLayout->setSpacing(4);
QLabel *savedLabel = new QLabel(Tr::tr("Or Load saved:"), this);
savedInstructionsLayout->addWidget(savedLabel);
m_commandsComboBox = new QComboBox(this);
m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
m_commandsComboBox->setEditable(true);
m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert);
m_commandsComboBox->lineEdit()->setPlaceholderText("Search or select instruction...");
m_commandsComboBox->lineEdit()->setPlaceholderText(Tr::tr("Search saved instructions..."));
QCompleter *completer = new QCompleter(this);
completer->setCompletionMode(QCompleter::PopupCompletion);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
m_commandsComboBox->setCompleter(completer);
instructionsLayout->addWidget(m_commandsComboBox);
savedInstructionsLayout->addWidget(m_commandsComboBox);
m_addCommandButton = new QToolButton(this);
m_addCommandButton->setText("+");
m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction"));
instructionsLayout->addWidget(m_addCommandButton);
m_addCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_addCommandButton);
m_editCommandButton = new QToolButton(this);
m_editCommandButton->setText("");
m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction"));
instructionsLayout->addWidget(m_editCommandButton);
m_editCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_editCommandButton);
m_deleteCommandButton = new QToolButton(this);
m_deleteCommandButton->setText("");
m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction"));
instructionsLayout->addWidget(m_deleteCommandButton);
m_deleteCommandButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_deleteCommandButton);
m_openFolderButton = new QToolButton(this);
m_openFolderButton->setText("📁");
m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder"));
instructionsLayout->addWidget(m_openFolderButton);
m_openFolderButton->setFocusPolicy(Qt::NoFocus);
savedInstructionsLayout->addWidget(m_openFolderButton);
mainLayout->addLayout(instructionsLayout);
mainLayout->addLayout(savedInstructionsLayout);
m_instructionsLabel = new QLabel(Tr::tr("Additional instructions (optional):"), this);
mainLayout->addWidget(m_instructionsLabel);
m_textEdit = new QPlainTextEdit(this);
m_textEdit->setMinimumHeight(100);
m_textEdit->setPlaceholderText(Tr::tr("Add extra details or modifications to the selected instruction..."));
connect(m_textEdit, &QPlainTextEdit::textChanged, this, &QuickRefactorDialog::updateDialogSize);
connect(
m_instructionEdit,
&QPlainTextEdit::textChanged,
this,
&QuickRefactorDialog::updateDialogSize);
connect(
m_commandsComboBox,
QOverload<int>::of(&QComboBox::currentIndexChanged),
@ -242,8 +258,6 @@ void QuickRefactorDialog::setupUi()
this,
&QuickRefactorDialog::onOpenInstructionsFolder);
mainLayout->addWidget(m_textEdit);
loadCustomCommands();
loadAvailableConfigurations();
@ -255,12 +269,23 @@ void QuickRefactorDialog::setupUi()
QDialogButtonBox *buttonBox
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QuickRefactorDialog::validateAndAccept);
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
mainLayout->addWidget(buttonBox);
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
setTabOrder(m_commandsComboBox, m_textEdit);
setTabOrder(m_textEdit, buttonBox);
if (okButton) {
okButton->installEventFilter(this);
}
if (cancelButton) {
cancelButton->installEventFilter(this);
}
setTabOrder(m_instructionEdit, m_commandsComboBox);
setTabOrder(m_commandsComboBox, okButton);
setTabOrder(okButton, cancelButton);
}
void QuickRefactorDialog::createActionButtons()
@ -295,27 +320,12 @@ void QuickRefactorDialog::createActionButtons()
QString QuickRefactorDialog::instructions() const
{
QString result;
CustomInstruction instruction = findCurrentInstruction();
if (!instruction.id.isEmpty()) {
result = instruction.body;
}
QString additionalText = m_textEdit->toPlainText().trimmed();
if (!additionalText.isEmpty()) {
if (!result.isEmpty()) {
result += "\n\n";
}
result += additionalText;
}
return result;
return m_instructionEdit->toPlainText().trimmed();
}
void QuickRefactorDialog::setInstructions(const QString &instructions)
{
m_textEdit->setPlainText(instructions);
m_instructionEdit->setPlainText(instructions);
}
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
@ -323,17 +333,33 @@ QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
return m_selectedAction;
}
void QuickRefactorDialog::keyPressEvent(QKeyEvent *event)
{
QDialog::keyPressEvent(event);
}
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_textEdit && event->type() == QEvent::KeyPress) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
if (keyEvent->modifiers() & Qt::ShiftModifier) {
return false;
}
accept();
return true;
if (watched == m_instructionEdit) {
if (keyEvent->key() == Qt::Key_Tab) {
m_commandsComboBox->setFocus();
return true;
}
}
if (watched == m_commandsComboBox || watched == m_commandsComboBox->lineEdit()) {
if (keyEvent->key() == Qt::Key_Tab) {
QPushButton *okButton = findChild<QPushButton *>();
if (okButton && okButton->text() == "OK") {
okButton->setFocus();
} else {
focusNextChild();
}
return true;
}
}
}
return QDialog::eventFilter(watched, event);
@ -343,8 +369,7 @@ void QuickRefactorDialog::useLastInstructions()
{
if (!m_lastInstructions.isEmpty()) {
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(m_lastInstructions);
m_instructionEdit->setPlainText(m_lastInstructions);
m_selectedAction = Action::RepeatLast;
}
accept();
@ -353,10 +378,10 @@ void QuickRefactorDialog::useLastInstructions()
void QuickRefactorDialog::useImproveCodeTemplate()
{
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(Tr::tr(
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
"Follow best practices for C++/Qt and fix any potential issues."));
m_instructionEdit->setPlainText(
Tr::tr(
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
"Follow best practices for C++/Qt and fix any potential issues."));
m_selectedAction = Action::ImproveCode;
accept();
}
@ -364,36 +389,29 @@ void QuickRefactorDialog::useImproveCodeTemplate()
void QuickRefactorDialog::useAlternativeSolutionTemplate()
{
m_commandsComboBox->setCurrentIndex(0);
m_commandsComboBox->clearEditText(); // Clear search text
m_textEdit->setPlainText(
Tr::tr("Suggest an alternative implementation approach for the selected code. "
"Provide a different solution that might be cleaner, more efficient, "
"or uses different Qt/C++ patterns or idioms."));
m_instructionEdit->setPlainText(
Tr::tr(
"Suggest an alternative implementation approach for the selected code. "
"Provide a different solution that might be cleaner, more efficient, "
"or uses different Qt/C++ patterns or idioms."));
m_selectedAction = Action::AlternativeSolution;
accept();
}
void QuickRefactorDialog::updateDialogSize()
{
QString text = m_textEdit->toPlainText();
QString text = m_instructionEdit->toPlainText();
QFontMetrics fm(m_textEdit->font());
QFontMetrics fm(m_instructionEdit->font());
QStringList lines = text.split('\n');
int lineCount = lines.size();
int lineCount = qMax(lines.size(), 3);
if (lineCount <= 1) {
int singleLineHeight = fm.height() + 10;
m_textEdit->setMinimumHeight(singleLineHeight);
m_textEdit->setMaximumHeight(singleLineHeight);
} else {
m_textEdit->setMaximumHeight(QWIDGETSIZE_MAX);
m_instructionEdit->setMaximumHeight(QWIDGETSIZE_MAX);
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 2) * lineHeight, 20 * lineHeight);
m_textEdit->setMinimumHeight(textEditHeight);
}
int lineHeight = fm.height() + 2;
int textEditHeight = qMin(qMax(lineCount, 3) * lineHeight, 15 * lineHeight);
m_instructionEdit->setMinimumHeight(textEditHeight);
int maxWidth = 500;
for (const QString &line : lines) {
@ -405,14 +423,7 @@ void QuickRefactorDialog::updateDialogSize()
QRect screenGeometry = screen->availableGeometry();
int newWidth = qMin(maxWidth + 40, screenGeometry.width() * 3 / 4);
int newHeight;
if (lineCount <= 1) {
newHeight = 150;
} else {
newHeight = m_textEdit->minimumHeight() + 150;
}
newHeight = qMin(newHeight, screenGeometry.height() * 3 / 4);
int newHeight = qMin(m_instructionEdit->minimumHeight() + 200, screenGeometry.height() * 3 / 4);
resize(newWidth, newHeight);
}
@ -420,22 +431,16 @@ void QuickRefactorDialog::updateDialogSize()
void QuickRefactorDialog::loadCustomCommands()
{
m_commandsComboBox->clear();
m_commandsComboBox->addItem("", QString()); // Empty item for no selection
m_commandsComboBox->addItem("", QString());
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
QStringList instructionNames;
int defaultInstructionIndex = -1;
for (int i = 0; i < instructions.size(); ++i) {
const CustomInstruction &instruction = instructions[i];
for (const CustomInstruction &instruction : instructions) {
m_commandsComboBox->addItem(instruction.name, instruction.id);
instructionNames.append(instruction.name);
if (instruction.isDefault) {
defaultInstructionIndex = i + 1;
}
}
if (m_commandsComboBox->completer()) {
@ -443,10 +448,6 @@ void QuickRefactorDialog::loadCustomCommands()
m_commandsComboBox->completer()->setModel(model);
}
if (defaultInstructionIndex > 0) {
m_commandsComboBox->setCurrentIndex(defaultInstructionIndex);
}
bool hasInstructions = !instructions.isEmpty();
m_editCommandButton->setEnabled(hasInstructions);
m_deleteCommandButton->setEnabled(hasInstructions);
@ -461,13 +462,13 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
auto &manager = CustomInstructionsManager::instance();
const QVector<CustomInstruction> &instructions = manager.instructions();
for (const CustomInstruction &instruction : instructions) {
if (instruction.name == currentText) {
return instruction;
}
}
int currentIndex = m_commandsComboBox->currentIndex();
if (currentIndex > 0) {
QString instructionId = m_commandsComboBox->itemData(currentIndex).toString();
@ -475,13 +476,20 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
return manager.getInstructionById(instructionId);
}
}
return CustomInstruction();
}
void QuickRefactorDialog::onCommandSelected(int index)
{
Q_UNUSED(index);
if (index <= 0) {
return;
}
CustomInstruction instruction = findCurrentInstruction();
if (!instruction.id.isEmpty()) {
m_instructionEdit->setPlainText(instruction.body);
}
}
void QuickRefactorDialog::onAddCustomCommand()
@ -493,10 +501,7 @@ void QuickRefactorDialog::onAddCustomCommand()
if (manager.saveInstruction(instruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(instruction.name);
m_textEdit->clear();
} else {
QMessageBox::warning(
this,
@ -509,10 +514,12 @@ void QuickRefactorDialog::onAddCustomCommand()
void QuickRefactorDialog::onEditCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to edit."));
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to edit."));
return;
}
@ -524,7 +531,6 @@ void QuickRefactorDialog::onEditCustomCommand()
if (manager.saveInstruction(updatedInstruction)) {
loadCustomCommands();
m_commandsComboBox->setCurrentText(updatedInstruction.name);
m_textEdit->clear();
} else {
QMessageBox::warning(
this,
@ -537,10 +543,12 @@ void QuickRefactorDialog::onEditCustomCommand()
void QuickRefactorDialog::onDeleteCustomCommand()
{
CustomInstruction instruction = findCurrentInstruction();
if (instruction.id.isEmpty()) {
QMessageBox::information(
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to delete."));
this,
Tr::tr("No Instruction Selected"),
Tr::tr("Please select an instruction to delete."));
return;
}
@ -569,12 +577,12 @@ void QuickRefactorDialog::onOpenInstructionsFolder()
{
QString path = QString("%1/qodeassist/quick_refactor/instructions")
.arg(Core::ICore::userResourcePath().toFSPathString());
QDir dir(path);
if (!dir.exists()) {
dir.mkpath(".");
}
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
QDesktopServices::openUrl(url);
}
@ -594,8 +602,8 @@ void QuickRefactorDialog::loadAvailableConfigurations()
auto &manager = Settings::ConfigurationManager::instance();
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs
= manager.configurations(Settings::ConfigurationType::QuickRefactor);
QVector<Settings::AIConfiguration> configs = manager.configurations(
Settings::ConfigurationType::QuickRefactor);
m_configComboBox->clear();
m_configComboBox->addItem(Tr::tr("Current"), QString());
@ -640,4 +648,20 @@ void QuickRefactorDialog::onConfigurationChanged(int index)
}
}
void QuickRefactorDialog::validateAndAccept()
{
QString instruction = m_instructionEdit->toPlainText().trimmed();
if (instruction.isEmpty()) {
QMessageBox::warning(
this,
Tr::tr("No Instruction"),
Tr::tr("Please type an instruction or select a saved one."));
m_instructionEdit->setFocus();
return;
}
accept();
}
} // namespace QodeAssist

View File

@ -27,6 +27,8 @@ class QPlainTextEdit;
class QToolButton;
class QLabel;
class QComboBox;
class QLineEdit;
class QFrame;
namespace QodeAssist {
@ -49,6 +51,7 @@ public:
QString selectedConfiguration() const;
bool eventFilter(QObject *watched, QEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
private slots:
void useLastInstructions();
@ -64,13 +67,14 @@ private slots:
void loadCustomCommands();
void loadAvailableConfigurations();
void onConfigurationChanged(int index);
void validateAndAccept();
private:
void setupUi();
void createActionButtons();
CustomInstruction findCurrentInstruction() const;
QPlainTextEdit *m_textEdit;
QPlainTextEdit *m_instructionEdit;
QToolButton *m_repeatButton;
QToolButton *m_improveButton;
QToolButton *m_alternativeButton;
@ -83,7 +87,6 @@ private:
QToolButton *m_thinkingButton;
QComboBox *m_commandsComboBox;
QComboBox *m_configComboBox;
QLabel *m_instructionsLabel;
Action m_selectedAction = Action::Custom;
QString m_lastInstructions;