Compare commits

..

25 Commits

Author SHA1 Message Date
Petr Mironychev
9a2ba08538 chore: Upgrade plugin to 0.9.11 2026-03-13 00:56:25 +01:00
Petr Mironychev
37084bec59 feat: Improve execute terminal command tool 2026-03-13 00:34:20 +01:00
Petr Mironychev
6910037e97 feat: Update models configuration 2026-03-12 23:58:06 +01:00
Petr Mironychev
a72cdd85a4 feat: Add support QtC 19
remove support QtC 17
2026-03-12 23:31:35 +01:00
lebedeviv1988
31b4e73af5 fix: Qt Creator 19 API breaking changes (#328)
* Inherits `QodeAssist::Settings::AgentRolesWidget` from `Core::IOptionsPageWidget`

* Adds `QodeAssist::Settings::showSettings` function and use it instead `Core::ICore::showOptionsDialog`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 16:00:51 +01:00
lebedeviv1988
088887c802 fix: enables the send message shortcut only for active chat (#322)
fix: Disables sending message shortcut instead of filtering in `Shortcut::activated` signal handler

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 12:01:14 +01:00
lebedeviv1988
b7a9787cc3 refactor: Refactors AgentRoleDialog's modes handling (#325)
* fix: Fixes `undefined-bool-conversion` compilation warning.

* refactor: Replaces `AgentRoleDialog::m_editMode` with `AgentRoleDialog::m_action`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 10:48:01 +01:00
Petr Mironychev
e2e13f0f38 refactor: Improve http client (#319) 2026-02-25 15:13:05 +01:00
Petr Mironychev
49ae335d7d chore: Update plugin to 0.9.10 2026-02-25 12:33:14 +01:00
Petr Mironychev
2ba58a403f refactor: UI for opening content from chat (#318)
* refactor: Changed options to opening images from chat
* refactor: Add customizable tooltip
2026-02-25 07:49:37 +01:00
Petr Mironychev
3de1619bf0 feat: Add file search to chat (#317) 2026-02-22 13:53:44 +01:00
Petr Mironychev
ec45067336 chore: Upgrade plugin to 0.9.9 version 2026-01-27 22:41:57 +01:00
Petr Mironychev
52fb65c5b1 feat: Add support QtCreator 18.0.2 2026-01-27 22:41:20 +01:00
Petr Mironychev
478f369ad2 feat: Add codestral and mistral quick setup 2026-01-27 22:41:02 +01:00
Petr Mironychev
762c965377 fix: Add preconditions for windows chat 2026-01-27 22:35:02 +01:00
Petr Mironychev
d2b93310e2 chore: Update plugin to 0.9.8 2026-01-20 20:00:49 +01:00
Petr Mironychev
f3b1e7f411 Add quick setup screenshot 2026-01-20 19:57:44 +01:00
Petr Mironychev
a55c6ccfdb feat: Add predefined templates 2026-01-20 19:54:16 +01:00
Petr Mironychev
b32433c336 refactor: Change quick refactor ui layout 2026-01-20 18:08:49 +01:00
Petr Mironychev
6f11260cd1 refactor: Change UI for fix behavior 2026-01-19 23:52:44 +01:00
Petr Mironychev
ddd6aba091 fix: Remove close chat action from editor context menu 2026-01-19 23:17:31 +01:00
Dinesh Bala
e3f464c54e fix: Create _content folder only when there is an attachment (#297) 2025-12-16 13:19:10 +01:00
Petr Mironychev
e86e58337a Update QodeAssist version range for Qt Creator 16.0.2 2025-12-15 01:00:00 +01:00
Petr Mironychev
dbd47387be chore: Update plugin to 0.9.7 2025-12-15 00:47:50 +01:00
Petr Mironychev
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
60 changed files with 2014 additions and 997 deletions

View File

@@ -46,16 +46,12 @@ jobs:
} }
qt_config: qt_config:
- { - {
qt_version: "6.8.3", qt_version: "6.10.1",
qt_creator_version: "16.0.2" qt_creator_version: "18.0.2"
} }
- { - {
qt_version: "6.9.2", qt_version: "6.10.2",
qt_creator_version: "17.0.2" qt_creator_version: "19.0.0"
}
- {
qt_version: "6.10.0",
qt_creator_version: "18.0.0"
} }
steps: steps:

View File

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

View File

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

View File

@@ -117,8 +117,10 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
QString contentFolder = QDir(dirPath).filePath(baseName + "_content"); QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
QString fullPath = QDir(contentFolder).filePath(image.storedPath); QString fullPath = QDir(contentFolder).filePath(image.storedPath);
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString(); imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
imageMap["filePath"] = fullPath;
} else { } else {
imageMap["imageUrl"] = QString(); imageMap["imageUrl"] = QString();
imageMap["filePath"] = QString();
} }
imagesList.append(imageMap); imagesList.append(imageMap);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024-2026 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -21,9 +21,12 @@
#include <QClipboard> #include <QClipboard>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir>
#include <QFile>
#include <QFileDialog> #include <QFileDialog>
#include <QFileInfo> #include <QFileInfo>
#include <QMessageBox> #include <QMessageBox>
#include <QTextStream>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -225,6 +228,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::refreshRules); &ChatRootView::refreshRules);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectAdded,
this,
&ChatRootView::openFilesChanged);
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::projectRemoved,
this,
&ChatRootView::openFilesChanged);
connect( connect(
&Settings::chatAssistantSettings().enableChatTools, &Settings::chatAssistantSettings().enableChatTools,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
@@ -735,7 +750,14 @@ void ChatRootView::openRulesFolder()
void ChatRootView::openSettings() void ChatRootView::openSettings()
{ {
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_CHAT_ASSISTANT_SETTINGS_PAGE_ID); Settings::showSettings(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() void ChatRootView::updateInputTokensCount()
@@ -788,6 +810,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
if (editor) { if (editor) {
m_currentEditors.removeOne(editor); m_currentEditors.removeOne(editor);
} }
emit openFilesChanged();
} }
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor) void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
@@ -805,6 +829,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
{ {
if (editor && editor->document()) { if (editor && editor->document()) {
m_currentEditors.append(editor); m_currentEditors.append(editor);
emit openFilesChanged();
} }
} }
@@ -1490,7 +1515,7 @@ QString ChatRootView::currentAgentRoleSystemPrompt() const
void ChatRootView::openAgentRolesSettings() void ChatRootView::openAgentRolesSettings()
{ {
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles")); Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
} }
void ChatRootView::compressCurrentChat() void ChatRootView::compressCurrentChat()

View File

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

View File

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

View File

@@ -0,0 +1,442 @@
/*
* 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::applyCurrentSelection(
const QString &text, int cursorPosition, bool useTools)
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size()) {
dismiss();
return {};
}
const QString textBefore = text.left(cursorPosition);
const int atIndex = textBefore.lastIndexOf('@');
if (atIndex < 0) {
dismiss();
return {};
}
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
QString replacement;
if (item.value("isProject").toBool()) {
replacement = QStringLiteral("@") + item.value("projectName").toString() + ":";
} else {
const QString currentQuery = textBefore.mid(atIndex + 1);
const QVariantMap result = handleFileSelection(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString(),
currentQuery,
useTools);
if (result.value("mode").toString() == "mention")
replacement = result.value("mentionText").toString();
}
const QString newText = text.left(atIndex) + replacement + text.mid(cursorPosition);
const int newCursorPosition = atIndex + replacement.length();
dismiss();
return {{"text", newText}, {"cursorPosition", newCursorPosition}};
}
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,86 @@
/*
* 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 QVariantMap applyCurrentSelection(
const QString &text, int cursorPosition, 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. * This file is part of QodeAssist.
* *
@@ -280,6 +280,10 @@ ChatRootView {
messageInput.cursorPosition = model.content.length messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx) 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) {
fileMentionPopup.updateSearch(query)
return
}
}
fileMentionPopup.dismiss()
}
Keys.onPressed: function(event) {
if (fileMentionPopup.visible) {
if (event.key === Qt.Key_Down) {
fileMentionPopup.moveDown()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
fileMentionPopup.moveUp()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.applyMentionSelection()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
fileMentionPopup.dismiss()
event.accepted = true
}
}
}
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
@@ -479,11 +514,8 @@ ChatRootView {
sequences: ["Ctrl+Return", "Ctrl+Enter"] sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut context: Qt.WindowShortcut
onActivated: { enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
if (messageInput.activeFocus && !Qt.inputMethod.visible) { onActivated: root.sendChatMessage()
root.sendChatMessage()
}
}
} }
function clearChat() { function clearChat() {
@@ -496,9 +528,19 @@ ChatRootView {
Qt.callLater(chatListView.positionViewAtEnd) Qt.callLater(chatListView.positionViewAtEnd)
} }
function applyMentionSelection() {
var result = fileMentionPopup.applyCurrentSelection(
messageInput.text, messageInput.cursorPosition, root.useTools)
if (result.text !== undefined) {
messageInput.text = result.text
messageInput.cursorPosition = result.cursorPosition
}
}
function sendChatMessage() { function sendChatMessage() {
root.sendMessage(messageInput.text) root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = "" messageInput.text = ""
fileMentionPopup.clearMentions()
scrollToBottom() scrollToBottom()
} }
@@ -572,6 +614,26 @@ ChatRootView {
infoToast.show(root.lastInfoMessage) infoToast.show(root.lastInfoMessage)
} }
} }
function onOpenFilesChanged() {
if (fileMentionPopup.visible)
Qt.callLater(fileMentionPopup.refreshSearch)
}
}
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
onSelectionRequested: root.applyMentionSelection()
onFileAttachRequested: function(filePaths) {
root.addFilesToAttachList(filePaths)
}
} }
Component.onCompleted: { 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. * This file is part of QodeAssist.
* *
@@ -51,6 +51,7 @@ Rectangle {
property int messageIndex: -1 property int messageIndex: -1
signal resetChatToMessage(int index) signal resetChatToMessage(int index)
signal openFileRequested(string filePath)
height: msgColumn.implicitHeight + 10 height: msgColumn.implicitHeight + 10
radius: 8 radius: 8
@@ -180,9 +181,12 @@ Rectangle {
onClicked: function() { onClicked: function() {
root.resetChatToMessage(root.messageIndex) root.resetChatToMessage(root.messageIndex)
} }
ToolTip.visible: hovered
ToolTip.text: qsTr("Reset chat to this message and edit") QoAToolTip {
ToolTip.delay: 500 visible: stopButtonId.hovered
text: qsTr("Reset chat to this message and edit")
delay: 500
}
} }
component TextComponent : TextBlock { component TextComponent : TextBlock {
@@ -204,6 +208,15 @@ Rectangle {
} }
} }
onLinkActivated: function(link) {
if (link.startsWith("file://")) {
var filePath = link.replace(/^file:\/\//, "")
root.openFileRequested(filePath)
} else {
Qt.openUrlExternally(link)
}
}
ChatUtils { ChatUtils {
id: utils id: utils
} }
@@ -257,33 +270,21 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) { if (mouse.modifiers & Qt.ShiftModifier) {
fileItem.openFileInExternalEditor()
} else {
fileItem.openFileInEditor() fileItem.openFileInEditor()
} else if (mouse.button === Qt.RightButton) {
attachmentContextMenu.popup()
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options") visible: attachFileMouseArea.containsMouse
ToolTip.delay: 500 text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
} delay: 500
Menu {
id: attachmentContextMenu
MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: fileItem.openFileInEditor()
}
MenuItem {
text: qsTr("Open in System Editor")
onTriggered: fileItem.openFileInExternalEditor()
} }
} }
} }
@@ -305,7 +306,7 @@ Rectangle {
FileItem { FileItem {
id: imageFileItem id: imageFileItem
filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : "" filePath: itemData.filePath || ""
} }
ColumnLayout { ColumnLayout {
@@ -361,33 +362,21 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) { if (mouse.modifiers & Qt.ShiftModifier) {
imageFileItem.openFileInExternalEditor()
} else {
imageFileItem.openFileInEditor() imageFileItem.openFileInEditor()
} else if (mouse.button === Qt.RightButton) {
imageContextMenu.popup()
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.text: qsTr("Left click: Open in System\nRight click: More options") visible: imageMouseArea.containsMouse
ToolTip.delay: 500 text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
} delay: 500
Menu {
id: imageContextMenu
MenuItem {
text: qsTr("Open in Qt Creator")
onTriggered: imageFileItem.openFileInEditor()
}
MenuItem {
text: qsTr("Open in System Viewer")
onTriggered: imageFileItem.openFileInExternalEditor()
} }
} }
} }

View File

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

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024-2026 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -21,6 +21,7 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import UIControls
Flow { Flow {
id: root id: root
@@ -78,9 +79,11 @@ Flow {
} }
} }
ToolTip.visible: containsMouse QoAToolTip {
ToolTip.delay: 500 visible: mouse.containsMouse
ToolTip.text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove" delay: 500
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
}
} }
Menu { Menu {

View File

@@ -0,0 +1,167 @@
/*
* 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
FileMentionItem {
id: root
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 36, 36 * 6) + 2
onCurrentIndexChanged: {
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
}
Rectangle {
id: background
anchors.fill: parent
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: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -170,28 +170,26 @@ void ConfigurationManager::selectModel()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue() : isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model : isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel : isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel; : m_generalSettings.caModel);
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
if (!provider->supportsModelListing()) { if (!provider->supportsModelListing()) {
m_generalSettings.showModelsNotSupportedDialog(targetSettings); m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return; return;
} }
const auto modelList = provider->getInstalledModels(providerUrl); provider->getInstalledModels(providerUrl)
.then(this, [this, targetSettings](const QList<QString> &modelList) {
if (modelList.isEmpty()) { if (modelList.isEmpty()) {
m_generalSettings.showModelsNotFoundDialog(targetSettings); m_generalSettings.showModelsNotFoundDialog(*targetSettings);
return; return;
} }
m_generalSettings.showSelectionDialog(
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() { modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
m_generalSettings.showSelectionDialog( });
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
});
} }
} }

View File

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

View File

@@ -125,16 +125,34 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
## Configuration ## 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 - **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server - **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider - **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support) - **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider - **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider - **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
- **LM Studio** - Local LLM provider - **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.) - **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 | | Qt Creator Version | QodeAssist Version |
|-------------------|-------------------| |-------------------|-------------------|
| 17.0.0+ | 0.6.0 - 0.x.x | | 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.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 | | 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 | | 15.0.1 | 0.4.8 - 0.5.1 |

View File

@@ -13,11 +13,12 @@ qt_add_qml_module(QodeAssistUIControls
qml/QoATextSlider.qml qml/QoATextSlider.qml
qml/QoAComboBox.qml qml/QoAComboBox.qml
qml/FadeListItemAnimation.qml qml/FadeListItemAnimation.qml
qml/QoASeparator.qml
qml/QoAToolTip.qml
RESOURCES RESOURCES
icons/dropdown-arrow-light.svg icons/dropdown-arrow-light.svg
icons/dropdown-arrow-dark.svg icons/dropdown-arrow-dark.svg
QML_FILES qml/QoASeparator.qml
) )
target_link_libraries(QodeAssistUIControls target_link_libraries(QodeAssistUIControls

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/>.
*/
import QtQuick
import QtQuick.Controls
ToolTip {
id: root
padding: 8
contentItem: Text {
text: root.text
font: root.font
color: palette.toolTipText
wrapMode: Text.Wrap
}
background: Item {
implicitWidth: bg.implicitWidth
implicitHeight: bg.implicitHeight
Rectangle {
anchors.fill: bg
anchors.margins: -2
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.12)
radius: 8
z: -2
}
Rectangle {
anchors.fill: bg
anchors.margins: -1
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.08)
radius: 7
z: -1
}
Rectangle {
id: bg
anchors.fill: parent
color: palette.toolTipBase
border.color: Qt.darker(palette.toolTipBase, 1.2)
border.width: 1
radius: 6
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0.0
to: 1.0
duration: 150
easing.type: Easing.OutQuad
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1.0
to: 0.0
duration: 100
easing.type: Easing.InQuad
}
}
}

View File

@@ -21,7 +21,6 @@
#include <QJsonDocument> #include <QJsonDocument>
#include <QMutexLocker> #include <QMutexLocker>
#include <QUuid>
#include <Logger.hpp> #include <Logger.hpp>
@@ -30,9 +29,7 @@ namespace QodeAssist::LLMCore {
HttpClient::HttpClient(QObject *parent) HttpClient::HttpClient(QObject *parent)
: QObject(parent) : QObject(parent)
, m_manager(new QNetworkAccessManager(this)) , m_manager(new QNetworkAccessManager(this))
{ {}
connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest);
}
HttpClient::~HttpClient() HttpClient::~HttpClient()
{ {
@@ -44,156 +41,96 @@ HttpClient::~HttpClient()
m_activeRequests.clear(); m_activeRequests.clear();
} }
void HttpClient::onSendRequest(const HttpRequest &request) QFuture<QByteArray> HttpClient::get(const QNetworkRequest &request)
{ {
QJsonDocument doc(request.payload); LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString()));
LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented)));
QNetworkReply *reply auto promise = std::make_shared<QPromise<QByteArray>>();
= m_manager->post(request.networkRequest, doc.toJson(QJsonDocument::Compact)); promise->start();
addActiveRequest(reply, request.requestId);
QNetworkReply *reply = m_manager->get(request);
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> HttpClient::post(const QNetworkRequest &request, const QJsonObject &payload)
{
QJsonDocument doc(payload);
LOG_MESSAGE(QString("HttpClient: POST %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
setupNonStreamingReply(reply, promise);
return promise->future();
}
QFuture<QByteArray> HttpClient::del(const QNetworkRequest &request,
std::optional<QJsonObject> payload)
{
auto promise = std::make_shared<QPromise<QByteArray>>();
promise->start();
QNetworkReply *reply;
if (payload) {
QJsonDocument doc(*payload);
LOG_MESSAGE(QString("HttpClient: DELETE %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
reply = m_manager->sendCustomRequest(request, "DELETE", doc.toJson(QJsonDocument::Compact));
} else {
LOG_MESSAGE(QString("HttpClient: DELETE %1").arg(request.url().toString()));
reply = m_manager->deleteResource(request);
}
setupNonStreamingReply(reply, promise);
return promise->future();
}
void HttpClient::setupNonStreamingReply(QNetworkReply *reply,
std::shared_ptr<QPromise<QByteArray>> promise)
{
connect(reply, &QNetworkReply::finished, this, [this, reply, promise]() {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseBody = reply->readAll();
QNetworkReply::NetworkError networkError = reply->error();
QString networkErrorString = reply->errorString();
reply->disconnect();
reply->deleteLater();
LOG_MESSAGE(
QString("HttpClient: Non-streaming request - HTTP Status: %1").arg(statusCode));
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
if (hasError) {
QString errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
LOG_MESSAGE(QString("HttpClient: Non-streaming request - Error: %1").arg(errorMsg));
promise->setException(
std::make_exception_ptr(std::runtime_error(errorMsg.toStdString())));
} else {
promise->addResult(responseBody);
}
promise->finish();
});
}
void HttpClient::postStreaming(const QString &requestId, const QNetworkRequest &request,
const QJsonObject &payload)
{
QJsonDocument doc(payload);
LOG_MESSAGE(QString("HttpClient: POST streaming %1, data: %2")
.arg(request.url().toString(), doc.toJson(QJsonDocument::Indented)));
QNetworkReply *reply = m_manager->post(request, doc.toJson(QJsonDocument::Compact));
addActiveRequest(reply, requestId);
connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead); connect(reply, &QNetworkReply::readyRead, this, &HttpClient::onReadyRead);
connect(reply, &QNetworkReply::finished, this, &HttpClient::onFinished); connect(reply, &QNetworkReply::finished, this, &HttpClient::onStreamingFinished);
}
void HttpClient::onReadyRead()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply || reply->isFinished())
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode >= 400) {
return;
}
QString requestId;
{
QMutexLocker locker(&m_mutex);
bool found = false;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply) {
requestId = it.key();
found = true;
break;
}
}
if (!found)
return;
}
if (requestId.isEmpty())
return;
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
emit dataReceived(requestId, data);
}
}
void HttpClient::onFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply)
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseBody = reply->readAll();
QNetworkReply::NetworkError networkError = reply->error();
QString networkErrorString = reply->errorString();
reply->disconnect();
QString requestId;
bool hasError = false;
QString errorMsg;
{
QMutexLocker locker(&m_mutex);
bool found = false;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply) {
requestId = it.key();
m_activeRequests.erase(it);
found = true;
break;
}
}
if (!found) {
reply->deleteLater();
return;
}
hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
if (hasError) {
errorMsg = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
}
LOG_MESSAGE(QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode));
if (!responseBody.isEmpty()) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3")
.arg(requestId)
.arg(responseBody.size())
.arg(QString::fromUtf8(responseBody)));
}
if (hasError) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, errorMsg));
}
}
reply->deleteLater();
if (!requestId.isEmpty()) {
emit requestFinished(requestId, !hasError, errorMsg);
}
}
QString HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
{
QMutexLocker locker(&m_mutex);
m_activeRequests[requestId] = reply;
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
return requestId;
}
QString HttpClient::parseErrorFromResponse(
int statusCode, const QByteArray &responseBody, const QString &networkErrorString)
{
QString errorMsg;
if (!responseBody.isEmpty()) {
QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody);
if (!errorDoc.isNull() && errorDoc.isObject()) {
QJsonObject errorObj = errorDoc.object();
if (errorObj.contains("error")) {
QJsonObject error = errorObj["error"].toObject();
QString message = error["message"].toString();
QString type = error["type"].toString();
QString code = error["code"].toString();
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message);
if (!type.isEmpty())
errorMsg += QString(" (type: %1)").arg(type);
if (!code.isEmpty())
errorMsg += QString(" (code: %1)").arg(code);
} else {
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
}
} else {
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
}
} else {
errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString);
}
return errorMsg;
} }
void HttpClient::cancelRequest(const QString &requestId) void HttpClient::cancelRequest(const QString &requestId)
@@ -212,4 +149,128 @@ void HttpClient::cancelRequest(const QString &requestId)
} }
} }
void HttpClient::onReadyRead()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply || reply->isFinished())
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode >= 400)
return;
QString requestId = findRequestId(reply);
if (requestId.isEmpty())
return;
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
emit dataReceived(requestId, data);
}
}
void HttpClient::onStreamingFinished()
{
QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender());
if (!reply)
return;
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QByteArray responseBody = reply->readAll();
QNetworkReply::NetworkError networkError = reply->error();
QString networkErrorString = reply->errorString();
reply->disconnect();
QString requestId;
std::optional<QString> error;
{
QMutexLocker locker(&m_mutex);
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply) {
requestId = it.key();
m_activeRequests.erase(it);
break;
}
}
if (requestId.isEmpty()) {
reply->deleteLater();
return;
}
bool hasError = (networkError != QNetworkReply::NoError) || (statusCode >= 400);
if (hasError) {
error = parseErrorFromResponse(statusCode, responseBody, networkErrorString);
}
LOG_MESSAGE(
QString("HttpClient: Request %1 - HTTP Status: %2").arg(requestId).arg(statusCode));
if (!responseBody.isEmpty()) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Response body (%2 bytes): %3")
.arg(requestId)
.arg(responseBody.size())
.arg(QString::fromUtf8(responseBody)));
}
if (error) {
LOG_MESSAGE(QString("HttpClient: Request %1 - Error: %2").arg(requestId, *error));
}
}
reply->deleteLater();
if (!requestId.isEmpty()) {
emit requestFinished(requestId, error);
}
}
QString HttpClient::findRequestId(QNetworkReply *reply)
{
QMutexLocker locker(&m_mutex);
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value() == reply)
return it.key();
}
return {};
}
void HttpClient::addActiveRequest(QNetworkReply *reply, const QString &requestId)
{
QMutexLocker locker(&m_mutex);
m_activeRequests[requestId] = reply;
LOG_MESSAGE(QString("HttpClient: Added active request: %1").arg(requestId));
}
QString HttpClient::parseErrorFromResponse(
int statusCode, const QByteArray &responseBody, const QString &networkErrorString)
{
if (!responseBody.isEmpty()) {
QJsonDocument errorDoc = QJsonDocument::fromJson(responseBody);
if (!errorDoc.isNull() && errorDoc.isObject()) {
QJsonObject errorObj = errorDoc.object();
if (errorObj.contains("error")) {
QJsonObject error = errorObj["error"].toObject();
QString message = error["message"].toString();
QString type = error["type"].toString();
QString code = error["code"].toString();
QString errorMsg = QString("HTTP %1: %2").arg(statusCode).arg(message);
if (!type.isEmpty())
errorMsg += QString(" (type: %1)").arg(type);
if (!code.isEmpty())
errorMsg += QString(" (code: %1)").arg(code);
return errorMsg;
}
return QString("HTTP %1: %2")
.arg(statusCode)
.arg(QString::fromUtf8(responseBody));
}
return QString("HTTP %1: %2").arg(statusCode).arg(QString::fromUtf8(responseBody));
}
return QString("HTTP %1: %2").arg(statusCode).arg(networkErrorString);
}
} // namespace QodeAssist::LLMCore } // namespace QodeAssist::LLMCore

View File

@@ -1,4 +1,4 @@
/* /*
* Copyright (C) 2025 Petr Mironychev * Copyright (C) 2025 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
@@ -19,24 +19,19 @@
#pragma once #pragma once
#include <optional>
#include <QFuture>
#include <QHash> #include <QHash>
#include <QJsonObject> #include <QJsonObject>
#include <QMap>
#include <QMutex> #include <QMutex>
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <QObject> #include <QObject>
#include <QUrl> #include <QPromise>
namespace QodeAssist::LLMCore { namespace QodeAssist::LLMCore {
struct HttpRequest
{
QNetworkRequest networkRequest;
QString requestId;
QJsonObject payload;
};
class HttpClient : public QObject class HttpClient : public QObject
{ {
Q_OBJECT Q_OBJECT
@@ -45,21 +40,33 @@ public:
HttpClient(QObject *parent = nullptr); HttpClient(QObject *parent = nullptr);
~HttpClient(); ~HttpClient();
// Non-streaming — return QFuture with full response
QFuture<QByteArray> get(const QNetworkRequest &request);
QFuture<QByteArray> post(const QNetworkRequest &request, const QJsonObject &payload);
QFuture<QByteArray> del(const QNetworkRequest &request,
std::optional<QJsonObject> payload = std::nullopt);
// Streaming — signal-based with requestId
void postStreaming(const QString &requestId, const QNetworkRequest &request,
const QJsonObject &payload);
void cancelRequest(const QString &requestId); void cancelRequest(const QString &requestId);
signals: signals:
void sendRequest(const QodeAssist::LLMCore::HttpRequest &request);
void dataReceived(const QString &requestId, const QByteArray &data); void dataReceived(const QString &requestId, const QByteArray &data);
void requestFinished(const QString &requestId, bool success, const QString &error); void requestFinished(const QString &requestId, std::optional<QString> error);
private slots: private slots:
void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request);
void onReadyRead(); void onReadyRead();
void onFinished(); void onStreamingFinished();
private: private:
QString addActiveRequest(QNetworkReply *reply, const QString &requestId); void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr<QPromise<QByteArray>> promise);
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody, const QString &networkErrorString);
QString findRequestId(QNetworkReply *reply);
void addActiveRequest(QNetworkReply *reply, const QString &requestId);
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody,
const QString &networkErrorString);
QNetworkAccessManager *m_manager; QNetworkAccessManager *m_manager;
QHash<QString, QNetworkReply *> m_activeRequests; QHash<QString, QNetworkReply *> m_activeRequests;

View File

@@ -19,6 +19,9 @@
#pragma once #pragma once
#include <optional>
#include <QFuture>
#include <utils/environment.h> #include <utils/environment.h>
#include <QNetworkRequest> #include <QNetworkRequest>
#include <QObject> #include <QObject>
@@ -57,7 +60,7 @@ public:
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) bool isThinkingEnabled)
= 0; = 0;
virtual QList<QString> getInstalledModels(const QString &url) = 0; virtual QFuture<QList<QString>> getInstalledModels(const QString &url) = 0;
virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0; virtual QList<QString> validateRequest(const QJsonObject &request, TemplateType type) = 0;
virtual QString apiKey() const = 0; virtual QString apiKey() const = 0;
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0; virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
@@ -81,7 +84,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
= 0; = 0;
virtual void onRequestFinished( virtual void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
= 0; = 0;
signals: signals:

View File

@@ -19,11 +19,9 @@
#include "ClaudeProvider.hpp" #include "ClaudeProvider.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
#include <QUrlQuery> #include <QUrlQuery>
#include "llmcore/ValidationUtils.hpp" #include "llmcore/ValidationUtils.hpp"
@@ -142,11 +140,8 @@ void ClaudeProvider::prepareRequest(
} }
} }
QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl) QFuture<QList<QString>> ClaudeProvider::getInstalledModels(const QString &baseUrl)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QUrl url(baseUrl + "/v1/models"); QUrl url(baseUrl + "/v1/models");
QUrlQuery query; QUrlQuery query;
query.addQueryItem("limit", "1000"); query.addQueryItem("limit", "1000");
@@ -160,32 +155,24 @@ QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
request.setRawHeader("x-api-key", apiKey().toUtf8()); request.setRawHeader("x-api-key", apiKey().toUtf8());
} }
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QEventLoop loop; QList<QString> models;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) { if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray(); QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) { for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject(); QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) { if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString(); models.append(modelObject["id"].toString());
models.append(modelId);
} }
} }
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
@@ -240,12 +227,9 @@ void ClaudeProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString())); LOG_MESSAGE(QString("ClaudeProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool ClaudeProvider::supportsTools() const bool ClaudeProvider::supportsTools() const
@@ -289,11 +273,11 @@ void ClaudeProvider::onDataReceived(
} }
void ClaudeProvider::onRequestFinished( void ClaudeProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -44,7 +44,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -65,8 +65,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -19,11 +19,9 @@
#include "GoogleAIProvider.hpp" #include "GoogleAIProvider.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
#include <QtCore/qurlquery.h> #include <QtCore/qurlquery.h>
#include "llmcore/ValidationUtils.hpp" #include "llmcore/ValidationUtils.hpp"
@@ -156,29 +154,17 @@ void GoogleAIProvider::prepareRequest(
} }
} }
QList<QString> GoogleAIProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> GoogleAIProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey())); QNetworkRequest request(QString("%1/models?key=%2").arg(url, apiKey()));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QEventLoop loop; QList<QString> models;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("models")) { if (jsonObject.contains("models")) {
QJsonArray modelArray = jsonObject["models"].toArray(); QJsonArray modelArray = jsonObject["models"].toArray();
models.clear();
for (const QJsonValue &value : modelArray) { for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject(); QJsonObject modelObject = value.toObject();
if (modelObject.contains("name")) { if (modelObject.contains("name")) {
@@ -190,12 +176,11 @@ QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
} }
} }
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> GoogleAIProvider::validateRequest( QList<QString> GoogleAIProvider::validateRequest(
@@ -254,13 +239,10 @@ void GoogleAIProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE( LOG_MESSAGE(
QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool GoogleAIProvider::supportsTools() const bool GoogleAIProvider::supportsTools() const
@@ -327,11 +309,11 @@ void GoogleAIProvider::onDataReceived(
} }
void GoogleAIProvider::onRequestFinished( void GoogleAIProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -62,8 +62,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -27,11 +27,9 @@
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp" #include "settings/ProviderSettings.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -71,35 +69,24 @@ bool LMStudioProvider::supportsModelListing() const
return true; return true;
} }
QList<QString> LMStudioProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> LMStudioProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1%2").arg(url, "/v1/models")); QNetworkRequest request(QString("%1%2").arg(url, "/v1/models"));
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QList<QString> models;
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
QJsonArray modelArray = jsonObject["data"].toArray();
QEventLoop loop; for (const QJsonValue &value : modelArray) {
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QJsonObject modelObject = value.toObject();
loop.exec(); models.append(modelObject["id"].toString());
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject();
QString modelId = modelObject["id"].toString();
models.append(modelId);
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> LMStudioProvider::validateRequest( QList<QString> LMStudioProvider::validateRequest(
@@ -149,13 +136,10 @@ void LMStudioProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE( LOG_MESSAGE(
QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString())); QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool LMStudioProvider::supportsTools() const bool LMStudioProvider::supportsTools() const
@@ -195,11 +179,11 @@ void LMStudioProvider::onDataReceived(
} }
void LMStudioProvider::onRequestFinished( void LMStudioProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -61,8 +61,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -26,11 +26,9 @@
#include "settings/QuickRefactorSettings.hpp" #include "settings/QuickRefactorSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -121,9 +119,9 @@ void LlamaCppProvider::prepareRequest(
} }
} }
QList<QString> LlamaCppProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> LlamaCppProvider::getInstalledModels(const QString &)
{ {
return {}; return QtFuture::makeReadyFuture(QList<QString>{});
} }
QList<QString> LlamaCppProvider::validateRequest( QList<QString> LlamaCppProvider::validateRequest(
@@ -192,13 +190,10 @@ void LlamaCppProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE( LOG_MESSAGE(
QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString())); QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool LlamaCppProvider::supportsTools() const bool LlamaCppProvider::supportsTools() const
@@ -250,11 +245,11 @@ void LlamaCppProvider::onDataReceived(
} }
void LlamaCppProvider::onRequestFinished( void LlamaCppProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -61,8 +61,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -27,11 +27,9 @@
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp" #include "settings/ProviderSettings.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -71,43 +69,32 @@ bool MistralAIProvider::supportsModelListing() const
return true; return true;
} }
QList<QString> MistralAIProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> MistralAIProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url)); QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) { if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
} }
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QEventLoop loop; QList<QString> models;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") { if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") {
QJsonArray modelArray = jsonObject["data"].toArray(); QJsonArray modelArray = jsonObject["data"].toArray();
for (const QJsonValue &value : modelArray) { for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject(); QJsonObject modelObject = value.toObject();
if (modelObject.contains("id")) { if (modelObject.contains("id")) {
QString modelId = modelObject["id"].toString(); models.append(modelObject["id"].toString());
models.append(modelId);
} }
} }
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> MistralAIProvider::validateRequest( QList<QString> MistralAIProvider::validateRequest(
@@ -170,13 +157,10 @@ void MistralAIProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE( LOG_MESSAGE(
QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool MistralAIProvider::supportsTools() const bool MistralAIProvider::supportsTools() const
@@ -216,11 +200,11 @@ void MistralAIProvider::onDataReceived(
} }
void MistralAIProvider::onRequestFinished( void MistralAIProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -61,8 +61,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -22,8 +22,6 @@
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
#include <QtCore/qeventloop.h>
#include "llmcore/ValidationUtils.hpp" #include "llmcore/ValidationUtils.hpp"
#include "logger/Logger.hpp" #include "logger/Logger.hpp"
@@ -147,35 +145,25 @@ void OllamaProvider::prepareRequest(
} }
} }
QList<QString> OllamaProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> OllamaProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1%2").arg(url, "/api/tags")); QNetworkRequest request(QString("%1%2").arg(url, "/api/tags"));
prepareNetworkRequest(request); prepareNetworkRequest(request);
QNetworkReply *reply = manager.get(request);
QEventLoop loop; return httpClient()->get(request).then([](const QByteArray &data) {
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QList<QString> models;
loop.exec(); QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
QJsonArray modelArray = jsonObject["models"].toArray(); QJsonArray modelArray = jsonObject["models"].toArray();
for (const QJsonValue &value : modelArray) { for (const QJsonValue &value : modelArray) {
QJsonObject modelObject = value.toObject(); QJsonObject modelObject = value.toObject();
QString modelName = modelObject["name"].toString(); models.append(modelObject["name"].toString());
models.append(modelName);
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
@@ -248,12 +236,9 @@ void OllamaProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString())); LOG_MESSAGE(QString("OllamaProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool OllamaProvider::supportsTools() const bool OllamaProvider::supportsTools() const
@@ -312,11 +297,11 @@ void OllamaProvider::onDataReceived(
} }
void OllamaProvider::onRequestFinished( void OllamaProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -44,7 +44,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -63,8 +63,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -122,9 +122,9 @@ void OpenAICompatProvider::prepareRequest(
} }
} }
QList<QString> OpenAICompatProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> OpenAICompatProvider::getInstalledModels(const QString &)
{ {
return QStringList(); return QtFuture::makeReadyFuture(QList<QString>{});
} }
QList<QString> OpenAICompatProvider::validateRequest( QList<QString> OpenAICompatProvider::validateRequest(
@@ -178,13 +178,10 @@ void OpenAICompatProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE( LOG_MESSAGE(
QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString())); QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool OpenAICompatProvider::supportsTools() const bool OpenAICompatProvider::supportsTools() const
@@ -224,11 +221,11 @@ void OpenAICompatProvider::onDataReceived(
} }
void OpenAICompatProvider::onRequestFinished( void OpenAICompatProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -61,8 +61,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -27,11 +27,9 @@
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProviderSettings.hpp" #include "settings/ProviderSettings.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -141,26 +139,17 @@ void OpenAIProvider::prepareRequest(
} }
} }
QList<QString> OpenAIProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> OpenAIProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url)); QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) { if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
} }
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QEventLoop loop; QList<QString> models;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
QByteArray responseData = reply->readAll();
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) { if (jsonObject.contains("data")) {
QJsonArray modelArray = jsonObject["data"].toArray(); QJsonArray modelArray = jsonObject["data"].toArray();
@@ -176,12 +165,11 @@ QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
} }
} }
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type) QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
@@ -235,12 +223,9 @@ void OpenAIProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString())); LOG_MESSAGE(QString("OpenAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
emit httpClient()->sendRequest(request); httpClient()->postStreaming(requestId, networkRequest, payload);
} }
bool OpenAIProvider::supportsTools() const bool OpenAIProvider::supportsTools() const
@@ -280,11 +265,11 @@ void OpenAIProvider::onDataReceived(
} }
void OpenAIProvider::onRequestFinished( void OpenAIProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -61,8 +61,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -28,11 +28,9 @@
#include "settings/ProviderSettings.hpp" #include "settings/ProviderSettings.hpp"
#include "settings/QuickRefactorSettings.hpp" #include "settings/QuickRefactorSettings.hpp"
#include <QEventLoop>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
#include <QNetworkReply>
namespace QodeAssist::Providers { namespace QodeAssist::Providers {
@@ -158,26 +156,17 @@ void OpenAIResponsesProvider::prepareRequest(
request["stream"] = true; request["stream"] = true;
} }
QList<QString> OpenAIResponsesProvider::getInstalledModels(const QString &url) QFuture<QList<QString>> OpenAIResponsesProvider::getInstalledModels(const QString &url)
{ {
QList<QString> models;
QNetworkAccessManager manager;
QNetworkRequest request(QString("%1/v1/models").arg(url)); QNetworkRequest request(QString("%1/v1/models").arg(url));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (!apiKey().isEmpty()) { if (!apiKey().isEmpty()) {
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8()); request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
} }
QNetworkReply *reply = manager.get(request); return httpClient()->get(request).then([](const QByteArray &data) {
QEventLoop loop; QList<QString> models;
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); const QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
loop.exec();
if (reply->error() == QNetworkReply::NoError) {
const QByteArray responseData = reply->readAll();
const QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
const QJsonObject jsonObject = jsonResponse.object();
if (jsonObject.contains("data")) { if (jsonObject.contains("data")) {
const QJsonArray modelArray = jsonObject["data"].toArray(); const QJsonArray modelArray = jsonObject["data"].toArray();
@@ -200,12 +189,11 @@ QList<QString> OpenAIResponsesProvider::getInstalledModels(const QString &url)
} }
} }
} }
} else { return models;
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString())); }).onFailed([](const std::exception &e) {
} LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
return QList<QString>{};
reply->deleteLater(); });
return models;
} }
QList<QString> OpenAIResponsesProvider::validateRequest( QList<QString> OpenAIResponsesProvider::validateRequest(
@@ -280,10 +268,7 @@ void OpenAIResponsesProvider::sendRequest(
QNetworkRequest networkRequest(url); QNetworkRequest networkRequest(url);
prepareNetworkRequest(networkRequest); prepareNetworkRequest(networkRequest);
LLMCore::HttpRequest httpClient()->postStreaming(requestId, networkRequest, payload);
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
emit httpClient()->sendRequest(request);
} }
bool OpenAIResponsesProvider::supportsTools() const bool OpenAIResponsesProvider::supportsTools() const
@@ -344,11 +329,11 @@ void OpenAIResponsesProvider::onDataReceived(
} }
void OpenAIResponsesProvider::onRequestFinished( void OpenAIResponsesProvider::onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error) const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
{ {
if (!success) { if (error) {
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, error)); LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, *error));
emit requestFailed(requestId, error); emit requestFailed(requestId, *error);
cleanupRequest(requestId); cleanupRequest(requestId);
return; return;
} }

View File

@@ -43,7 +43,7 @@ public:
LLMCore::RequestType type, LLMCore::RequestType type,
bool isToolsEnabled, bool isToolsEnabled,
bool isThinkingEnabled) override; bool isThinkingEnabled) override;
QList<QString> getInstalledModels(const QString &url) override; QFuture<QList<QString>> getInstalledModels(const QString &url) override;
QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override; QList<QString> validateRequest(const QJsonObject &request, LLMCore::TemplateType type) override;
QString apiKey() const override; QString apiKey() const override;
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override; void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
@@ -62,8 +62,7 @@ public slots:
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override; const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
void onRequestFinished( void onRequestFinished(
const QodeAssist::LLMCore::RequestID &requestId, const QodeAssist::LLMCore::RequestID &requestId,
bool success, std::optional<QString> error) override;
const QString &error) override;
private slots: private slots:
void onToolExecutionComplete( void onToolExecutionComplete(

View File

@@ -236,7 +236,7 @@ public:
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat")); closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon()); closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
closeChatViewAction.addOnTriggered(this, [this] { closeChatViewAction.addOnTriggered(this, [this] {
if (m_chatView->isVisible()) { if (m_chatView && m_chatView->isActive() && m_chatView->isVisible()) {
m_chatView->close(); m_chatView->close();
} }
}); });
@@ -250,8 +250,6 @@ public:
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE); editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(showChatViewAction.command(), editorContextMenu->addAction(showChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE); Core::Constants::G_DEFAULT_THREE);
editorContextMenu->addAction(closeChatViewAction.command(),
Core::Constants::G_DEFAULT_THREE);
} }
Chat::ChatFileManager::cleanupGlobalIntermediateStorage(); Chat::ChatFileManager::cleanupGlobalIntermediateStorage();

View File

@@ -29,21 +29,24 @@
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
AgentRoleDialog::AgentRoleDialog(QWidget *parent) AgentRoleDialog::AgentRoleDialog(Action action, QWidget *parent)
: QDialog(parent) : QDialog{parent}
, m_editMode(false) , m_action{action}
{ {
setWindowTitle(tr("Add Agent Role")); auto getTitle = [](Action action) {
setupUI(); switch(action)
} {
case Action::Add:
return tr("Add Agent Role");
case Action::Duplicate:
return tr("Duplicate Agent Role");
case Action::Edit:
return tr("Edit Agent Role");
}
};
AgentRoleDialog::AgentRoleDialog(const AgentRole &role, bool editMode, QWidget *parent) setWindowTitle(getTitle(action));
: QDialog(parent)
, m_editMode(editMode)
{
setWindowTitle(editMode ? tr("Edit Agent Role") : tr("Duplicate Agent Role"));
setupUI(); setupUI();
setRole(role);
} }
void AgentRoleDialog::setupUI() void AgentRoleDialog::setupUI()
@@ -83,7 +86,7 @@ void AgentRoleDialog::setupUI()
connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput); connect(m_idEdit, &QLineEdit::textChanged, this, &AgentRoleDialog::validateInput);
connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput); connect(m_systemPromptEdit, &QTextEdit::textChanged, this, &AgentRoleDialog::validateInput);
if (m_editMode) { if (m_action == Action::Edit) {
m_idEdit->setEnabled(false); m_idEdit->setEnabled(false);
m_idEdit->setToolTip(tr("ID cannot be changed for existing roles")); m_idEdit->setToolTip(tr("ID cannot be changed for existing roles"));
} }

View File

@@ -34,8 +34,18 @@ class AgentRoleDialog : public QDialog
Q_OBJECT Q_OBJECT
public: public:
explicit AgentRoleDialog(QWidget *parent = nullptr); enum class Action {
explicit AgentRoleDialog(const AgentRole &role, bool editMode = true, QWidget *parent = nullptr); Add,
Duplicate,
Edit,
};
explicit AgentRoleDialog(Action action, QWidget *parent = nullptr);
explicit AgentRoleDialog(const AgentRole &role, Action action, QWidget *parent = nullptr)
: AgentRoleDialog{action, parent}
{
setRole(role);
}
AgentRole getRole() const; AgentRole getRole() const;
void setRole(const AgentRole &role); void setRole(const AgentRole &role);
@@ -49,7 +59,7 @@ private:
QTextEdit *m_descriptionEdit = nullptr; QTextEdit *m_descriptionEdit = nullptr;
QTextEdit *m_systemPromptEdit = nullptr; QTextEdit *m_systemPromptEdit = nullptr;
QDialogButtonBox *m_buttonBox = nullptr; QDialogButtonBox *m_buttonBox = nullptr;
bool m_editMode = false; Action m_action;
}; };
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

@@ -34,13 +34,6 @@
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
AgentRolesWidget::AgentRolesWidget(QWidget *parent)
: QWidget(parent)
{
setupUI();
loadRoles();
}
void AgentRolesWidget::setupUI() void AgentRolesWidget::setupUI()
{ {
auto *mainLayout = new QVBoxLayout(this); auto *mainLayout = new QVBoxLayout(this);
@@ -129,7 +122,7 @@ void AgentRolesWidget::updateButtons()
void AgentRolesWidget::onAddRole() void AgentRolesWidget::onAddRole()
{ {
AgentRoleDialog dialog(this); AgentRoleDialog dialog{AgentRoleDialog::Action::Add, this};
if (dialog.exec() != QDialog::Accepted) if (dialog.exec() != QDialog::Accepted)
return; return;
@@ -170,7 +163,7 @@ void AgentRolesWidget::onEditRole()
return; return;
} }
AgentRoleDialog dialog(role, this); AgentRoleDialog dialog{role, AgentRoleDialog::Action::Edit, this};
if (dialog.exec() != QDialog::Accepted) if (dialog.exec() != QDialog::Accepted)
return; return;
@@ -203,7 +196,7 @@ void AgentRolesWidget::onDuplicateRole()
role.id = baseId + QString::number(counter++); role.id = baseId + QString::number(counter++);
} }
AgentRoleDialog dialog(role, false, this); AgentRoleDialog dialog{role, AgentRoleDialog::Action::Duplicate, this};
if (dialog.exec() != QDialog::Accepted) if (dialog.exec() != QDialog::Accepted)
return; return;

View File

@@ -19,19 +19,23 @@
#pragma once #pragma once
#include <QWidget> #include <coreplugin/dialogs/ioptionspage.h>
class QListWidget; class QListWidget;
class QPushButton; class QPushButton;
namespace QodeAssist::Settings { namespace QodeAssist::Settings {
class AgentRolesWidget : public QWidget class AgentRolesWidget : public Core::IOptionsPageWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit AgentRolesWidget(QWidget *parent = nullptr); explicit AgentRolesWidget()
{
setupUI();
loadRoles();
}
private: private:
void setupUI(); void setupUI();

View File

@@ -43,6 +43,106 @@ ConfigurationManager &ConfigurationManager::instance()
return instance; return instance;
} }
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
ConfigurationType type)
{
QVector<AIConfiguration> presets;
AIConfiguration claudeOpus;
claudeOpus.id = "preset_claude_opus";
claudeOpus.name = "Claude Opus 4.6";
claudeOpus.provider = "Claude";
claudeOpus.model = "claude-opus-4-6";
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.6";
claudeSonnet.provider = "Claude";
claudeSonnet.model = "claude-sonnet-4-6";
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-latest";
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 gpt;
gpt.id = "preset_gpt";
gpt.name = "gpt-5.4";
gpt.provider = "OpenAI Responses";
gpt.model = "gpt-5.4";
gpt.url = "https://api.openai.com";
gpt.endpointMode = "Auto";
gpt.customEndpoint = "";
gpt.templateName = "OpenAI Responses";
gpt.type = type;
gpt.isPredefined = true;
presets.append(claudeSonnet);
presets.append(claudeHaiku);
presets.append(claudeOpus);
presets.append(gpt);
presets.append(codestral);
presets.append(mistral);
presets.append(geminiFlash);
return presets;
}
QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const QString ConfigurationManager::configurationTypeToString(ConfigurationType type) const
{ {
switch (type) { switch (type) {
@@ -94,6 +194,9 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
configs->clear(); configs->clear();
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
configs->append(predefinedConfigs);
if (!ensureDirectoryExists(type)) { if (!ensureDirectoryExists(type)) {
LOG_MESSAGE("Failed to create configuration directory"); LOG_MESSAGE("Failed to create configuration directory");
return false; return false;
@@ -131,6 +234,7 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
config.customEndpoint = obj["customEndpoint"].toString(); config.customEndpoint = obj["customEndpoint"].toString();
config.type = type; config.type = type;
config.formatVersion = obj.value("formatVersion").toInt(1); config.formatVersion = obj.value("formatVersion").toInt(1);
config.isPredefined = false;
if (config.id.isEmpty() || config.name.isEmpty()) { if (config.id.isEmpty() || config.name.isEmpty()) {
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName())); 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) 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)); QDir dir(getConfigurationDirectory(type));
QStringList filters; QStringList filters;
filters << QString("*_%1.json").arg(id); filters << QString("*_%1.json").arg(id);

View File

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

View File

@@ -23,6 +23,7 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <utils/layoutbuilder.h> #include <utils/layoutbuilder.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <QComboBox>
#include <QDesktopServices> #include <QDesktopServices>
#include <QDir> #include <QDir>
#include <QInputDialog> #include <QInputDialog>
@@ -88,6 +89,27 @@ GeneralSettings::GeneralSettings()
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS; resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE; 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"); initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
ccProvider.setReadOnly(true); ccProvider.setReadOnly(true);
@@ -127,6 +149,7 @@ GeneralSettings::GeneralSettings()
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
ccLoadConfig.m_buttonText = TrConstants::LOAD_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_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
ccOpenConfigFolder.m_isCompact = true; ccOpenConfigFolder.m_isCompact = true;
@@ -218,6 +241,7 @@ GeneralSettings::GeneralSettings()
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
caLoadConfig.m_buttonText = TrConstants::LOAD_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_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
caOpenConfigFolder.m_isCompact = true; caOpenConfigFolder.m_isCompact = true;
@@ -262,6 +286,7 @@ GeneralSettings::GeneralSettings()
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG; qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
qrLoadConfig.m_buttonText = TrConstants::LOAD_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_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon(); qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
qrOpenConfigFolder.m_isCompact = true; qrOpenConfigFolder.m_isCompact = true;
@@ -325,17 +350,24 @@ GeneralSettings::GeneralSettings()
title(TrConstants::CODE_COMPLETION), title(TrConstants::CODE_COMPLETION),
Column{ Column{
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}}, Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}},
ccGrid, ccGrid,
Row{specifyPreset1, preset1Language, Stretch{1}}, Row{specifyPreset1, preset1Language, Stretch{1}},
ccPreset1Grid}}; ccPreset1Grid}};
auto caGroup = Group{ auto caGroup = Group{
title(TrConstants::CHAT_ASSISTANT), 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{ auto qrGroup = Group{
title(TrConstants::QUICK_REFACTOR), 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{ auto rootLayout = Column{
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}}, Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
@@ -420,7 +452,7 @@ void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect)
connect(configureApiKeyBtn, &QPushButton::clicked, &dialog, [&dialog]() { connect(configureApiKeyBtn, &QPushButton::clicked, &dialog, [&dialog]() {
dialog.close(); dialog.close();
Core::ICore::showOptionsDialog(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID); Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
}); });
dialog.buttonLayout()->addWidget(selectProviderBtn); dialog.buttonLayout()->addWidget(selectProviderBtn);
@@ -570,6 +602,33 @@ void GeneralSettings::setupConnections()
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() { connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent()); 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, []() {
Settings::showSettings(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, []() {
Settings::showSettings(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, []() {
Settings::showSettings(Constants::QODE_ASSIST_PROVIDER_SETTINGS_PAGE_ID);
});
connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() { connect(&specifyPreset1, &Utils::BoolAspect::volatileValueChanged, this, [this]() {
updatePreset1Visiblity(specifyPreset1.volatileValue()); updatePreset1Visiblity(specifyPreset1.volatileValue());
@@ -776,11 +835,33 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION); SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
dialog.addLabel(TrConstants::SELECT_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(); dialog.addSpacing();
QStringList configNames; QStringList configNames;
for (const AIConfiguration &config : configs) { 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()); auto configList = dialog.addComboBox(configNames, QString());
@@ -790,9 +871,31 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
auto *okButton = new QPushButton(TrConstants::OK); auto *okButton = new QPushButton(TrConstants::OK);
auto *cancelButton = new QPushButton(TrConstants::CANCEL); 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, [&]() { connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
int currentIndex = configList->currentIndex(); int currentIndex = configList->currentIndex();
if (currentIndex >= 0 && currentIndex < configs.size()) { 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( QMessageBox::StandardButton reply = QMessageBox::question(
&dialog, &dialog,
TrConstants::DELETE_CONFIGURATION, TrConstants::DELETE_CONFIGURATION,
@@ -800,7 +903,6 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
QMessageBox::Yes | QMessageBox::No); QMessageBox::Yes | QMessageBox::No);
if (reply == QMessageBox::Yes) { if (reply == QMessageBox::Yes) {
const AIConfiguration &configToDelete = configs[currentIndex];
if (manager.deleteConfiguration(configToDelete.id, type)) { if (manager.deleteConfiguration(configToDelete.id, type)) {
dialog.accept(); dialog.accept();
onLoadConfiguration(prefix); onLoadConfiguration(prefix);
@@ -860,6 +962,73 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
dialog.exec(); 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 class GeneralSettingsPage : public Core::IOptionsPage
{ {
public: public:
@@ -877,5 +1046,29 @@ public:
}; };
const GeneralSettingsPage generalSettingsPage; const GeneralSettingsPage generalSettingsPage;
/*!
\sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()}
\note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version
*/
void showSettings(const Utils::Id page)
{
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
Core::ICore::showSettings(page);
#else
Core::ICore::showOptionsDialog(page);
#endif
}
/*!
\sa {Core::ICore::showOptionsDialog()}, {Core::ICore::showSettings()}
\note This function was added to fix Qt Creator API broken changes in v19.0.0-beta2 version
*/
void showSettings(const Utils::Id page, Utils::Id item)
{
#if QODEASSIST_QT_CREATOR_VERSION >= QT_VERSION_CHECK(18, 0, 83)
Core::ICore::showSettings(page, item);
#else
Core::ICore::showOptionsDialog(page, item);
#endif
}
} // namespace QodeAssist::Settings } // namespace QodeAssist::Settings

View File

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

View File

@@ -116,6 +116,7 @@ const char CA_ALLOWED_TERMINAL_COMMANDS[] = "QodeAssist.caAllowedTerminalCommand
const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux"; const char CA_ALLOWED_TERMINAL_COMMANDS_LINUX[] = "QodeAssist.caAllowedTerminalCommandsLinux";
const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS"; const char CA_ALLOWED_TERMINAL_COMMANDS_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS";
const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows"; const char CA_ALLOWED_TERMINAL_COMMANDS_WINDOWS[] = "QodeAssist.caAllowedTerminalCommandsWindows";
const char CA_TERMINAL_COMMAND_TIMEOUT[] = "QodeAssist.caTerminalCommandTimeout";
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions"; const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId"; const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";

View File

@@ -128,6 +128,14 @@ ToolsSettings::ToolsSettings()
allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay); allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where"); allowedTerminalCommandsWindows.setDefaultValue("git, dir, type, findstr, where");
terminalCommandTimeout.setSettingsKey(Constants::CA_TERMINAL_COMMAND_TIMEOUT);
terminalCommandTimeout.setLabelText(Tr::tr("Command Timeout (seconds)"));
terminalCommandTimeout.setToolTip(
Tr::tr("Maximum time in seconds to wait for a terminal command to complete. "
"Increase for long-running commands like builds."));
terminalCommandTimeout.setRange(5, 3600);
terminalCommandTimeout.setDefaultValue(30);
resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults"); resetToDefaults.m_buttonText = Tr::tr("Reset Page to Defaults");
readSettings(); readSettings();
@@ -167,6 +175,7 @@ ToolsSettings::ToolsSettings()
enableTerminalCommandTool, enableTerminalCommandTool,
enableTodoTool, enableTodoTool,
currentOsCommands, currentOsCommands,
terminalCommandTimeout,
autoApplyFileEdits}}, autoApplyFileEdits}},
Stretch{1}}; Stretch{1}};
}); });
@@ -203,6 +212,7 @@ void ToolsSettings::resetSettingsToDefaults()
resetAspect(allowedTerminalCommandsLinux); resetAspect(allowedTerminalCommandsLinux);
resetAspect(allowedTerminalCommandsMacOS); resetAspect(allowedTerminalCommandsMacOS);
resetAspect(allowedTerminalCommandsWindows); resetAspect(allowedTerminalCommandsWindows);
resetAspect(terminalCommandTimeout);
writeSettings(); writeSettings();
} }
} }

View File

@@ -45,6 +45,7 @@ public:
Utils::StringAspect allowedTerminalCommandsLinux{this}; Utils::StringAspect allowedTerminalCommandsLinux{this};
Utils::StringAspect allowedTerminalCommandsMacOS{this}; Utils::StringAspect allowedTerminalCommandsMacOS{this};
Utils::StringAspect allowedTerminalCommandsWindows{this}; Utils::StringAspect allowedTerminalCommandsWindows{this};
Utils::IntegerAspect terminalCommandTimeout{this};
Utils::BoolAspect autoApplyFileEdits{this}; Utils::BoolAspect autoApplyFileEdits{this};
private: private:

View File

@@ -80,7 +80,10 @@ public:
return true; return true;
} }
QList<QString> getInstalledModels(const QString &url) override { return {}; } QFuture<QList<QString>> getInstalledModels(const QString &) override
{
return QtFuture::makeReadyFuture(QList<QString>{});
}
QStringList validateRequest( QStringList validateRequest(
const QJsonObject &request, LLMCore::TemplateType templateType) override const QJsonObject &request, LLMCore::TemplateType templateType) override

View File

@@ -32,6 +32,8 @@
#include <QSharedPointer> #include <QSharedPointer>
#include <QTimer> #include <QTimer>
#include <atomic>
namespace QodeAssist::Tools { namespace QodeAssist::Tools {
ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent) ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent)
@@ -188,54 +190,66 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
QFuture<QString> future = promise->future(); QFuture<QString> future = promise->future();
promise->start(); promise->start();
auto resolved = std::make_shared<std::atomic<bool>>(false);
QProcess *process = new QProcess(); QProcess *process = new QProcess();
process->setWorkingDirectory(workingDir); process->setWorkingDirectory(workingDir);
process->setProcessChannelMode(QProcess::MergedChannels); process->setProcessChannelMode(QProcess::MergedChannels);
process->setReadChannel(QProcess::StandardOutput); process->setReadChannel(QProcess::StandardOutput);
const int timeoutMs = commandTimeoutMs();
QTimer *timeoutTimer = new QTimer(); QTimer *timeoutTimer = new QTimer();
timeoutTimer->setSingleShot(true); timeoutTimer->setSingleShot(true);
timeoutTimer->setInterval(COMMAND_TIMEOUT_MS); timeoutTimer->setInterval(timeoutMs);
auto outputSize = QSharedPointer<qint64>::create(0); QObject::connect(timeoutTimer, &QTimer::timeout, [process, promise, resolved, command, args, timeoutTimer, timeoutMs]() {
if (*resolved)
return;
*resolved = true;
QObject::connect(timeoutTimer, &QTimer::timeout, [process, promise, command, args, timeoutTimer]() {
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1 %2' timed out after %3ms") LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1 %2' timed out after %3ms")
.arg(command) .arg(command)
.arg(args) .arg(args)
.arg(COMMAND_TIMEOUT_MS)); .arg(timeoutMs));
process->terminate(); process->terminate();
QTimer::singleShot(1000, process, [process]() { QTimer::singleShot(1000, process, [process]() {
if (process->state() == QProcess::Running) { if (process->state() != QProcess::NotRunning) {
LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout"); LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout");
process->kill(); process->kill();
} }
process->deleteLater();
}); });
promise->addResult(QString("Error: Command '%1 %2' timed out after %3 seconds. " promise->addResult(QString("Error: Command '%1 %2' timed out after %3 seconds. "
"The process has been terminated.") "The process has been terminated.")
.arg(command) .arg(command)
.arg(args.isEmpty() ? "" : args) .arg(args.isEmpty() ? "" : args)
.arg(COMMAND_TIMEOUT_MS / 1000)); .arg(timeoutMs / 1000));
promise->finish(); promise->finish();
process->deleteLater();
timeoutTimer->deleteLater(); timeoutTimer->deleteLater();
}); });
QObject::connect( QObject::connect(
process, process,
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[this, process, promise, command, args, timeoutTimer, outputSize]( [this, process, promise, resolved, command, args, timeoutTimer](
int exitCode, QProcess::ExitStatus exitStatus) { int exitCode, QProcess::ExitStatus exitStatus) {
if (*resolved) {
process->deleteLater();
return;
}
*resolved = true;
timeoutTimer->stop(); timeoutTimer->stop();
timeoutTimer->deleteLater(); timeoutTimer->deleteLater();
const QByteArray rawOutput = process->readAll(); const QByteArray rawOutput = process->readAll();
*outputSize += rawOutput.size(); const qint64 outputSize = rawOutput.size();
const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), *outputSize); const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), outputSize);
const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args); const QString fullCommand = args.isEmpty() ? command : QString("%1 %2").arg(command).arg(args);
@@ -244,7 +258,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' completed " LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' completed "
"successfully (output size: %2 bytes)") "successfully (output size: %2 bytes)")
.arg(fullCommand) .arg(fullCommand)
.arg(*outputSize)); .arg(outputSize));
promise->addResult( promise->addResult(
QString("Command '%1' executed successfully.\n\nOutput:\n%2") QString("Command '%1' executed successfully.\n\nOutput:\n%2")
.arg(fullCommand) .arg(fullCommand)
@@ -254,7 +268,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
"exit code %2 (output size: %3 bytes)") "exit code %2 (output size: %3 bytes)")
.arg(fullCommand) .arg(fullCommand)
.arg(exitCode) .arg(exitCode)
.arg(*outputSize)); .arg(outputSize));
promise->addResult( promise->addResult(
QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3") QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3")
.arg(fullCommand) .arg(fullCommand)
@@ -265,7 +279,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was " LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was "
"terminated (output size: %2 bytes)") "terminated (output size: %2 bytes)")
.arg(fullCommand) .arg(fullCommand)
.arg(*outputSize)); .arg(outputSize));
const QString error = process->errorString(); const QString error = process->errorString();
promise->addResult( promise->addResult(
QString("Command '%1' crashed or was terminated.\n\nError: %2\n\nOutput:\n%3") QString("Command '%1' crashed or was terminated.\n\nError: %2\n\nOutput:\n%3")
@@ -278,11 +292,13 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
process->deleteLater(); process->deleteLater();
}); });
QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args, timeoutTimer]( QObject::connect(process, &QProcess::errorOccurred, [process, promise, resolved, command, args, timeoutTimer](
QProcess::ProcessError error) { QProcess::ProcessError error) {
if (promise->future().isFinished()) { if (*resolved) {
process->deleteLater();
return; return;
} }
*resolved = true;
timeoutTimer->stop(); timeoutTimer->stop();
timeoutTimer->deleteLater(); timeoutTimer->deleteLater();
@@ -292,7 +308,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
.arg(fullCommand) .arg(fullCommand)
.arg(error) .arg(error)
.arg(process->errorString())); .arg(process->errorString()));
QString errorMessage; QString errorMessage;
switch (error) { switch (error) {
case QProcess::FailedToStart: case QProcess::FailedToStart:
@@ -318,71 +334,46 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
.arg(process->errorString()); .arg(process->errorString());
break; break;
} }
promise->addResult(QString("Error: %1").arg(errorMessage)); promise->addResult(QString("Error: %1").arg(errorMessage));
promise->finish(); promise->finish();
process->deleteLater(); process->deleteLater();
}); });
QString fullCommand = command; QStringList argsList;
if (!args.isEmpty()) { if (!args.isEmpty()) {
fullCommand += " " + args; argsList = QProcess::splitCommand(args);
} }
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
static const QStringList windowsBuiltinCommands = { static const QStringList windowsBuiltinCommands = {
"dir", "type", "del", "copy", "move", "ren", "rename", "dir", "type", "del", "copy", "move", "ren", "rename",
"md", "mkdir", "rd", "rmdir", "cd", "chdir", "cls", "echo", "md", "mkdir", "rd", "rmdir", "cd", "chdir", "cls", "echo",
"set", "path", "prompt", "ver", "vol", "date", "time" "set", "path", "prompt", "ver", "vol", "date", "time"
}; };
const QString lowerCommand = command.toLower(); const QString lowerCommand = command.toLower();
const bool isBuiltin = windowsBuiltinCommands.contains(lowerCommand); const bool isBuiltin = windowsBuiltinCommands.contains(lowerCommand);
if (isBuiltin) { if (isBuiltin) {
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing Windows builtin command '%1' via cmd.exe") LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing Windows builtin command '%1' via cmd.exe")
.arg(command)); .arg(command));
process->start("cmd.exe", QStringList() << "/c" << fullCommand); QStringList cmdArgs;
cmdArgs << "/c" << command;
cmdArgs.append(argsList);
process->start("cmd.exe", cmdArgs);
} else { } else {
#endif process->start(command, argsList);
QStringList splitCommand = QProcess::splitCommand(fullCommand);
if (splitCommand.isEmpty()) {
LOG_MESSAGE("ExecuteTerminalCommandTool: Failed to parse command");
promise->addResult(QString("Error: Failed to parse command '%1'").arg(fullCommand));
promise->finish();
process->deleteLater();
timeoutTimer->deleteLater();
return future;
}
const QString program = splitCommand.takeFirst();
process->start(program, splitCommand);
#ifdef Q_OS_WIN
} }
#else
process->start(command, argsList);
#endif #endif
if (!process->waitForStarted(PROCESS_START_TIMEOUT_MS)) {
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Failed to start command '%1' within %2ms")
.arg(fullCommand)
.arg(PROCESS_START_TIMEOUT_MS));
const QString errorString = process->errorString();
promise->addResult(QString("Error: Failed to start command '%1': %2\n\n"
"Possible reasons:\n"
"- Command not found in PATH\n"
"- Insufficient permissions\n"
"- Invalid command syntax")
.arg(fullCommand)
.arg(errorString));
promise->finish();
process->deleteLater();
timeoutTimer->deleteLater();
return future;
}
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process started successfully (PID: %1)")
.arg(process->processId()));
timeoutTimer->start(); timeoutTimer->start();
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process start requested for '%1'")
.arg(command));
return future; return future;
} }
@@ -414,19 +405,27 @@ bool ExecuteTerminalCommandTool::areArgumentsSafe(const QString &args) const
return true; return true;
} }
// Check for null bytes
if (args.contains(QChar('\0'))) {
LOG_MESSAGE("ExecuteTerminalCommandTool: Null byte found in args");
return false;
}
static const QStringList dangerousPatterns = { static const QStringList dangerousPatterns = {
";", // Command separator ";", // Command separator
"&&", // AND operator "&", // Command separator / background execution
"||", // OR operator
"|", // Pipe operator "|", // Pipe operator
">", // Output redirection ">", // Output redirection
">>", // Append redirection
"<", // Input redirection "<", // Input redirection
"`", // Command substitution "`", // Command substitution
"$(", // Command substitution "$(", // Command substitution
"$()", // Command substitution "${", // Variable expansion
"\\n", // Newline (could start new command) "\n", // Newline (could start new command)
"\\r" // Carriage return "\r", // Carriage return
#ifdef Q_OS_WIN
"^", // Escape character in cmd.exe (can bypass other checks)
"%", // Environment variable expansion on Windows
#endif
}; };
for (const QString &pattern : dangerousPatterns) { for (const QString &pattern : dangerousPatterns) {
@@ -456,9 +455,6 @@ QString ExecuteTerminalCommandTool::sanitizeOutput(const QString &output, qint64
QStringList ExecuteTerminalCommandTool::getAllowedCommands() const QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
{ {
static QString cachedCommandsStr;
static QStringList cachedCommands;
QString commandsStr; QString commandsStr;
#ifdef Q_OS_LINUX #ifdef Q_OS_LINUX
@@ -471,28 +467,27 @@ QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback
#endif #endif
if (commandsStr == cachedCommandsStr && !cachedCommands.isEmpty()) {
return cachedCommands;
}
cachedCommandsStr = commandsStr;
cachedCommands.clear();
if (commandsStr.isEmpty()) { if (commandsStr.isEmpty()) {
return QStringList(); return QStringList();
} }
QStringList result;
const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts); const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts);
cachedCommands.reserve(rawCommands.size()); result.reserve(rawCommands.size());
for (const QString &cmd : rawCommands) { for (const QString &cmd : rawCommands) {
const QString trimmed = cmd.trimmed(); const QString trimmed = cmd.trimmed();
if (!trimmed.isEmpty()) { if (!trimmed.isEmpty()) {
cachedCommands.append(trimmed); result.append(trimmed);
} }
} }
return cachedCommands; return result;
}
int ExecuteTerminalCommandTool::commandTimeoutMs() const
{
return Settings::toolsSettings().terminalCommandTimeout() * 1000;
} }
QString ExecuteTerminalCommandTool::getCommandDescription() const QString ExecuteTerminalCommandTool::getCommandDescription() const
@@ -518,7 +513,7 @@ QString ExecuteTerminalCommandTool::getCommandDescription() const
"Commands have a %2 second timeout. " "Commands have a %2 second timeout. "
"Returns the command output (stdout and stderr) or an error message if the command fails.%3") "Returns the command output (stdout and stderr) or an error message if the command fails.%3")
.arg(allowedList) .arg(allowedList)
.arg(COMMAND_TIMEOUT_MS / 1000) .arg(commandTimeoutMs() / 1000)
.arg(osInfo); .arg(osInfo);
} }

View File

@@ -46,12 +46,12 @@ private:
QString getCommandDescription() const; QString getCommandDescription() const;
QString sanitizeOutput(const QString &output, qint64 maxSize) const; QString sanitizeOutput(const QString &output, qint64 maxSize) const;
int commandTimeoutMs() const;
// Constants for production safety // Constants for production safety
static constexpr int COMMAND_TIMEOUT_MS = 30000; // 30 seconds
static constexpr qint64 MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10 MB static constexpr qint64 MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10 MB
static constexpr int MAX_COMMAND_LENGTH = 1024; static constexpr int MAX_COMMAND_LENGTH = 1024;
static constexpr int MAX_ARGS_LENGTH = 4096; static constexpr int MAX_ARGS_LENGTH = 4096;
static constexpr int PROCESS_START_TIMEOUT_MS = 3000;
}; };
} // namespace QodeAssist::Tools } // namespace QodeAssist::Tools

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

View File

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

View File

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