mirror of
https://github.com/Palm1r/QodeAssist.git
synced 2026-06-13 17:59:15 -04:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a2ba08538 | ||
|
|
37084bec59 | ||
|
|
6910037e97 | ||
|
|
a72cdd85a4 | ||
|
|
31b4e73af5 | ||
|
|
088887c802 | ||
|
|
b7a9787cc3 | ||
|
|
e2e13f0f38 | ||
|
|
49ae335d7d | ||
|
|
2ba58a403f | ||
|
|
3de1619bf0 | ||
|
|
ec45067336 | ||
|
|
52fb65c5b1 | ||
|
|
478f369ad2 | ||
|
|
762c965377 | ||
|
|
d2b93310e2 | ||
|
|
f3b1e7f411 | ||
|
|
a55c6ccfdb | ||
|
|
b32433c336 | ||
|
|
6f11260cd1 | ||
|
|
ddd6aba091 | ||
|
|
e3f464c54e | ||
|
|
e86e58337a |
8
.github/workflows/build_cmake.yml
vendored
8
.github/workflows/build_cmake.yml
vendored
@@ -46,12 +46,12 @@ jobs:
|
||||
}
|
||||
qt_config:
|
||||
- {
|
||||
qt_version: "6.9.2",
|
||||
qt_creator_version: "17.0.2"
|
||||
qt_version: "6.10.1",
|
||||
qt_creator_version: "18.0.2"
|
||||
}
|
||||
- {
|
||||
qt_version: "6.10.1",
|
||||
qt_creator_version: "18.0.1"
|
||||
qt_version: "6.10.2",
|
||||
qt_creator_version: "19.0.0"
|
||||
}
|
||||
|
||||
steps:
|
||||
|
||||
@@ -142,7 +142,6 @@ add_qtc_plugin(QodeAssist
|
||||
|
||||
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
|
||||
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
|
||||
tools/ToolHandler.hpp tools/ToolHandler.cpp
|
||||
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
|
||||
tools/ToolsManager.hpp tools/ToolsManager.cpp
|
||||
|
||||
@@ -20,6 +20,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
|
||||
qml/controls/AttachedFilesPlace.qml
|
||||
qml/controls/BottomBar.qml
|
||||
qml/controls/FileMentionPopup.qml
|
||||
qml/controls/FileEditsActionBar.qml
|
||||
qml/controls/ContextViewer.qml
|
||||
qml/controls/Toast.qml
|
||||
@@ -68,6 +69,7 @@ qt_add_qml_module(QodeAssistChatView
|
||||
FileItem.hpp FileItem.cpp
|
||||
ChatFileManager.hpp ChatFileManager.cpp
|
||||
ChatCompressor.hpp ChatCompressor.cpp
|
||||
FileMentionItem.hpp FileMentionItem.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistChatView
|
||||
|
||||
@@ -117,8 +117,10 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
||||
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
||||
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
||||
imageMap["filePath"] = fullPath;
|
||||
} else {
|
||||
imageMap["imageUrl"] = QString();
|
||||
imageMap["filePath"] = QString();
|
||||
}
|
||||
|
||||
imagesList.append(imageMap);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -21,9 +21,12 @@
|
||||
|
||||
#include <QClipboard>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QMessageBox>
|
||||
#include <QTextStream>
|
||||
|
||||
#include <coreplugin/editormanager/editormanager.h>
|
||||
#include <coreplugin/icore.h>
|
||||
@@ -225,6 +228,18 @@ ChatRootView::ChatRootView(QQuickItem *parent)
|
||||
this,
|
||||
&ChatRootView::refreshRules);
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::projectAdded,
|
||||
this,
|
||||
&ChatRootView::openFilesChanged);
|
||||
|
||||
connect(
|
||||
ProjectExplorer::ProjectManager::instance(),
|
||||
&ProjectExplorer::ProjectManager::projectRemoved,
|
||||
this,
|
||||
&ChatRootView::openFilesChanged);
|
||||
|
||||
connect(
|
||||
&Settings::chatAssistantSettings().enableChatTools,
|
||||
&Utils::BaseAspect::changed,
|
||||
@@ -735,7 +750,14 @@ void ChatRootView::openRulesFolder()
|
||||
|
||||
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()
|
||||
@@ -788,6 +810,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
|
||||
if (editor) {
|
||||
m_currentEditors.removeOne(editor);
|
||||
}
|
||||
|
||||
emit openFilesChanged();
|
||||
}
|
||||
|
||||
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
|
||||
@@ -805,6 +829,7 @@ void ChatRootView::onEditorCreated(Core::IEditor *editor, const Utils::FilePath
|
||||
{
|
||||
if (editor && editor->document()) {
|
||||
m_currentEditors.append(editor);
|
||||
emit openFilesChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1490,7 +1515,7 @@ QString ChatRootView::currentAgentRoleSystemPrompt() const
|
||||
|
||||
void ChatRootView::openAgentRolesSettings()
|
||||
{
|
||||
Core::ICore::showOptionsDialog(Utils::Id("QodeAssist.AgentRoles"));
|
||||
Settings::showSettings(Utils::Id("QodeAssist.AgentRoles"));
|
||||
}
|
||||
|
||||
void ChatRootView::compressCurrentChat()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <QQuickItem>
|
||||
#include <QVariantList>
|
||||
|
||||
#include "ChatFileManager.hpp"
|
||||
#include "ChatModel.hpp"
|
||||
@@ -104,6 +105,8 @@ public:
|
||||
Q_INVOKABLE void openRulesFolder();
|
||||
Q_INVOKABLE void openSettings();
|
||||
|
||||
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||
|
||||
Q_INVOKABLE void updateInputTokensCount();
|
||||
int inputTokensCount() const;
|
||||
|
||||
@@ -222,6 +225,8 @@ signals:
|
||||
void compressionCompleted(const QString &compressedChatPath);
|
||||
void compressionFailed(const QString &error);
|
||||
|
||||
void openFilesChanged();
|
||||
|
||||
private:
|
||||
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||
QString getChatsHistoryDir() const;
|
||||
|
||||
@@ -38,14 +38,6 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
||||
return {false, "Failed to create directory structure"};
|
||||
}
|
||||
|
||||
QString contentFolder = getChatContentFolder(filePath);
|
||||
QDir dir;
|
||||
if (!dir.exists(contentFolder)) {
|
||||
if (!dir.mkpath(contentFolder)) {
|
||||
LOG_MESSAGE(QString("Warning: Failed to create content folder: %1").arg(contentFolder));
|
||||
}
|
||||
}
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||
@@ -88,21 +80,22 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
||||
return {true, QString()};
|
||||
}
|
||||
|
||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
|
||||
QJsonObject ChatSerializer::serializeMessage(
|
||||
const ChatModel::Message &message, const QString &chatFilePath)
|
||||
{
|
||||
QJsonObject messageObj;
|
||||
messageObj["role"] = static_cast<int>(message.role);
|
||||
messageObj["content"] = message.content;
|
||||
messageObj["id"] = message.id;
|
||||
|
||||
|
||||
if (message.isRedacted) {
|
||||
messageObj["isRedacted"] = true;
|
||||
}
|
||||
|
||||
|
||||
if (!message.signature.isEmpty()) {
|
||||
messageObj["signature"] = message.signature;
|
||||
}
|
||||
|
||||
|
||||
if (!message.attachments.isEmpty()) {
|
||||
QJsonArray attachmentsArray;
|
||||
for (const auto &attachment : message.attachments) {
|
||||
@@ -113,7 +106,7 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
||||
}
|
||||
messageObj["attachments"] = attachmentsArray;
|
||||
}
|
||||
|
||||
|
||||
if (!message.images.isEmpty()) {
|
||||
QJsonArray imagesArray;
|
||||
for (const auto &image : message.images) {
|
||||
@@ -125,11 +118,12 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message,
|
||||
}
|
||||
messageObj["images"] = imagesArray;
|
||||
}
|
||||
|
||||
|
||||
return messageObj;
|
||||
}
|
||||
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
|
||||
ChatModel::Message ChatSerializer::deserializeMessage(
|
||||
const QJsonObject &json, const QString &chatFilePath)
|
||||
{
|
||||
ChatModel::Message message;
|
||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||
@@ -137,7 +131,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
||||
message.id = json["id"].toString();
|
||||
message.isRedacted = json["isRedacted"].toBool(false);
|
||||
message.signature = json["signature"].toString();
|
||||
|
||||
|
||||
if (json.contains("attachments")) {
|
||||
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||
for (const auto &attachmentValue : attachmentsArray) {
|
||||
@@ -148,7 +142,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
||||
message.attachments.append(attachment);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (json.contains("images")) {
|
||||
QJsonArray imagesArray = json["images"].toArray();
|
||||
for (const auto &imageValue : imagesArray) {
|
||||
@@ -160,7 +154,7 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, c
|
||||
message.images.append(image);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -178,7 +172,8 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString
|
||||
return root;
|
||||
}
|
||||
|
||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
||||
bool ChatSerializer::deserializeChat(
|
||||
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
||||
{
|
||||
QJsonArray messagesArray = json["messages"].toArray();
|
||||
QVector<ChatModel::Message> messages;
|
||||
@@ -189,17 +184,24 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json,
|
||||
}
|
||||
|
||||
model->clear();
|
||||
|
||||
|
||||
model->setLoadingFromHistory(true);
|
||||
|
||||
|
||||
for (const auto &message : messages) {
|
||||
model->addMessage(message.content, message.role, message.id, message.attachments, message.images, message.isRedacted, message.signature);
|
||||
model->addMessage(
|
||||
message.content,
|
||||
message.role,
|
||||
message.id,
|
||||
message.attachments,
|
||||
message.images,
|
||||
message.isRedacted,
|
||||
message.signature);
|
||||
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||
.arg(message.images.size())
|
||||
.arg(message.isRedacted)
|
||||
.arg(message.signature.length()));
|
||||
}
|
||||
|
||||
|
||||
model->setLoadingFromHistory(false);
|
||||
|
||||
return true;
|
||||
@@ -217,12 +219,14 @@ bool ChatSerializer::validateVersion(const QString &version)
|
||||
if (version == VERSION) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
if (version == "0.1") {
|
||||
LOG_MESSAGE("Loading chat from old format 0.1 - images folder structure has changed from _images to _content");
|
||||
LOG_MESSAGE(
|
||||
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||
"to _content");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -234,10 +238,11 @@ QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||
return QDir(dirPath).filePath(baseName + "_content");
|
||||
}
|
||||
|
||||
bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
|
||||
const QString &fileName,
|
||||
const QString &base64Data,
|
||||
QString &storedPath)
|
||||
bool ChatSerializer::saveContentToStorage(
|
||||
const QString &chatFilePath,
|
||||
const QString &fileName,
|
||||
const QString &base64Data,
|
||||
QString &storedPath)
|
||||
{
|
||||
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||
QDir dir;
|
||||
@@ -247,34 +252,34 @@ bool ChatSerializer::saveContentToStorage(const QString &chatFilePath,
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
QFileInfo originalFileInfo(fileName);
|
||||
QString extension = originalFileInfo.suffix();
|
||||
QString baseName = originalFileInfo.completeBaseName();
|
||||
QString uniqueName = QString("%1_%2.%3")
|
||||
.arg(baseName)
|
||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
||||
.arg(extension);
|
||||
|
||||
.arg(baseName)
|
||||
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
||||
.arg(extension);
|
||||
|
||||
QString fullPath = QDir(contentFolder).filePath(uniqueName);
|
||||
|
||||
|
||||
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||
QFile file(fullPath);
|
||||
if (!file.open(QIODevice::WriteOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (file.write(contentData) == -1) {
|
||||
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
file.close();
|
||||
|
||||
|
||||
storedPath = uniqueName;
|
||||
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -282,16 +287,16 @@ QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, cons
|
||||
{
|
||||
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||
|
||||
|
||||
QFile file(fullPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
|
||||
return QString();
|
||||
}
|
||||
|
||||
|
||||
QByteArray contentData = file.readAll();
|
||||
file.close();
|
||||
|
||||
|
||||
return contentData.toBase64();
|
||||
}
|
||||
|
||||
|
||||
442
ChatView/FileMentionItem.cpp
Normal file
442
ChatView/FileMentionItem.cpp
Normal 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 ¤tQuery,
|
||||
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
|
||||
86
ChatView/FileMentionItem.hpp
Normal file
86
ChatView/FileMentionItem.hpp
Normal 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 ¤tQuery,
|
||||
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
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -280,6 +280,10 @@ ChatRootView {
|
||||
messageInput.cursorPosition = model.content.length
|
||||
root.chatModel.resetModelTo(idx)
|
||||
}
|
||||
|
||||
onOpenFileRequested: function(filePath) {
|
||||
root.openFileInEditor(filePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,7 +372,38 @@ ChatRootView {
|
||||
}
|
||||
}
|
||||
|
||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
||||
onTextChanged: {
|
||||
root.calculateMessageTokensCount(messageInput.text)
|
||||
var cursorPos = messageInput.cursorPosition
|
||||
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||
var atIndex = textBefore.lastIndexOf('@')
|
||||
if (atIndex >= 0) {
|
||||
var query = textBefore.substring(atIndex + 1)
|
||||
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||
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 {
|
||||
anchors.fill: parent
|
||||
@@ -479,11 +514,8 @@ ChatRootView {
|
||||
|
||||
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||
context: Qt.WindowShortcut
|
||||
onActivated: {
|
||||
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
|
||||
root.sendChatMessage()
|
||||
}
|
||||
}
|
||||
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
||||
onActivated: root.sendChatMessage()
|
||||
}
|
||||
|
||||
function clearChat() {
|
||||
@@ -496,9 +528,19 @@ ChatRootView {
|
||||
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() {
|
||||
root.sendMessage(messageInput.text)
|
||||
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||
messageInput.text = ""
|
||||
fileMentionPopup.clearMentions()
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
@@ -572,6 +614,26 @@ ChatRootView {
|
||||
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: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -51,6 +51,7 @@ Rectangle {
|
||||
property int messageIndex: -1
|
||||
|
||||
signal resetChatToMessage(int index)
|
||||
signal openFileRequested(string filePath)
|
||||
|
||||
height: msgColumn.implicitHeight + 10
|
||||
radius: 8
|
||||
@@ -180,9 +181,12 @@ Rectangle {
|
||||
onClicked: function() {
|
||||
root.resetChatToMessage(root.messageIndex)
|
||||
}
|
||||
ToolTip.visible: hovered
|
||||
ToolTip.text: qsTr("Reset chat to this message and edit")
|
||||
ToolTip.delay: 500
|
||||
|
||||
QoAToolTip {
|
||||
visible: stopButtonId.hovered
|
||||
text: qsTr("Reset chat to this message and edit")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
id: utils
|
||||
}
|
||||
@@ -257,33 +270,21 @@ Rectangle {
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||
fileItem.openFileInExternalEditor()
|
||||
} else {
|
||||
fileItem.openFileInEditor()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
attachmentContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.text: qsTr("Left click: Open in Qt Creator\nRight click: More options")
|
||||
ToolTip.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()
|
||||
QoAToolTip {
|
||||
visible: attachFileMouseArea.containsMouse
|
||||
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +306,7 @@ Rectangle {
|
||||
|
||||
FileItem {
|
||||
id: imageFileItem
|
||||
filePath: itemData.imageUrl ? itemData.imageUrl.toString().replace("file://", "") : ""
|
||||
filePath: itemData.filePath || ""
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -361,33 +362,21 @@ Rectangle {
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
acceptedButtons: Qt.LeftButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onClicked: (mouse) => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||
imageFileItem.openFileInExternalEditor()
|
||||
} else {
|
||||
imageFileItem.openFileInEditor()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
imageContextMenu.popup()
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.text: qsTr("Left click: Open in System\nRight click: More options")
|
||||
ToolTip.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()
|
||||
QoAToolTip {
|
||||
visible: imageMouseArea.containsMouse
|
||||
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||
delay: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -29,8 +29,6 @@ TextEdit {
|
||||
selectionColor: palette.highlight
|
||||
color: palette.text
|
||||
|
||||
onLinkActivated: (link) => Qt.openUrlExternally(link)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2024-2025 Petr Mironychev
|
||||
* Copyright (C) 2024-2026 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
*
|
||||
@@ -21,6 +21,7 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import ChatView
|
||||
import UIControls
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
@@ -78,9 +79,11 @@ Flow {
|
||||
}
|
||||
}
|
||||
|
||||
ToolTip.visible: containsMouse
|
||||
ToolTip.delay: 500
|
||||
ToolTip.text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||
QoAToolTip {
|
||||
visible: mouse.containsMouse
|
||||
delay: 500
|
||||
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||
}
|
||||
}
|
||||
|
||||
Menu {
|
||||
|
||||
167
ChatView/qml/controls/FileMentionPopup.qml
Normal file
167
ChatView/qml/controls/FileMentionPopup.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,28 +170,26 @@ void ConfigurationManager::selectModel()
|
||||
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
|
||||
: m_generalSettings.caUrl.volatileValue();
|
||||
|
||||
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: isQuickRefactor ? m_generalSettings.qrModel
|
||||
: m_generalSettings.caModel;
|
||||
auto *targetSettings = &(isCodeCompletion ? m_generalSettings.ccModel
|
||||
: isPreset1 ? m_generalSettings.ccPreset1Model
|
||||
: isQuickRefactor ? m_generalSettings.qrModel
|
||||
: m_generalSettings.caModel);
|
||||
|
||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||
if (!provider->supportsModelListing()) {
|
||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
||||
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
||||
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
provider->getInstalledModels(providerUrl)
|
||||
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
||||
if (modelList.isEmpty()) {
|
||||
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
||||
return;
|
||||
}
|
||||
m_generalSettings.showSelectionDialog(
|
||||
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"Id" : "qodeassist",
|
||||
"Name" : "QodeAssist",
|
||||
"Version" : "0.9.7",
|
||||
"Version" : "0.9.11",
|
||||
"CompatVersion" : "${IDE_VERSION}",
|
||||
"Vendor" : "Petr Mironychev",
|
||||
"VendorId" : "petrmironychev",
|
||||
|
||||
32
README.md
32
README.md
@@ -125,16 +125,34 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
|
||||
|
||||
## Configuration
|
||||
|
||||
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
|
||||
### Quick Setup (Recommended for Beginners)
|
||||
|
||||
### Supported Providers
|
||||
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
|
||||
<details>
|
||||
<summary>Quick setup: (click to expand)</summary>
|
||||
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
|
||||
</details>
|
||||
|
||||
1. **Open QodeAssist Settings**
|
||||
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
||||
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
||||
- **OpenAI** (gpt-5.2-codex)
|
||||
- **Mistral AI** (Codestral 2501)
|
||||
- **Google AI** (Gemini 2.5 Flash)
|
||||
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||
|
||||
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||
|
||||
### Manual Provider Configuration
|
||||
|
||||
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
|
||||
|
||||
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
|
||||
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
|
||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
|
||||
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider (includes Responses API support)
|
||||
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
|
||||
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
|
||||
- **[Anthropic Claude](docs/claude-configuration.md)** - Cloud provider (manual setup)
|
||||
- **[OpenAI](docs/openai-configuration.md)** - Cloud provider (includes Responses API support)
|
||||
- **[Mistral AI](docs/mistral-configuration.md)** - Cloud provider
|
||||
- **[Google AI](docs/google-ai-configuration.md)** - Cloud provider
|
||||
- **LM Studio** - Local LLM provider
|
||||
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
|
||||
|
||||
@@ -360,7 +378,7 @@ See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](do
|
||||
| Qt Creator Version | QodeAssist Version |
|
||||
|-------------------|-------------------|
|
||||
| 17.0.0+ | 0.6.0 - 0.x.x |
|
||||
| 16.0.2 | 0.5.13 - 0.x.x |
|
||||
| 16.0.2 | 0.5.13 - 0.9.6 |
|
||||
| 16.0.1 | 0.5.7 - 0.5.13 |
|
||||
| 16.0.0 | 0.5.2 - 0.5.6 |
|
||||
| 15.0.1 | 0.4.8 - 0.5.1 |
|
||||
|
||||
@@ -13,11 +13,12 @@ qt_add_qml_module(QodeAssistUIControls
|
||||
qml/QoATextSlider.qml
|
||||
qml/QoAComboBox.qml
|
||||
qml/FadeListItemAnimation.qml
|
||||
qml/QoASeparator.qml
|
||||
qml/QoAToolTip.qml
|
||||
|
||||
RESOURCES
|
||||
icons/dropdown-arrow-light.svg
|
||||
icons/dropdown-arrow-dark.svg
|
||||
QML_FILES qml/QoASeparator.qml
|
||||
)
|
||||
|
||||
target_link_libraries(QodeAssistUIControls
|
||||
|
||||
84
UIControls/qml/QoAToolTip.qml
Normal file
84
UIControls/qml/QoAToolTip.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QMutexLocker>
|
||||
#include <QUuid>
|
||||
|
||||
#include <Logger.hpp>
|
||||
|
||||
@@ -30,9 +29,7 @@ namespace QodeAssist::LLMCore {
|
||||
HttpClient::HttpClient(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_manager(new QNetworkAccessManager(this))
|
||||
{
|
||||
connect(this, &HttpClient::sendRequest, this, &HttpClient::onSendRequest);
|
||||
}
|
||||
{}
|
||||
|
||||
HttpClient::~HttpClient()
|
||||
{
|
||||
@@ -44,156 +41,96 @@ HttpClient::~HttpClient()
|
||||
m_activeRequests.clear();
|
||||
}
|
||||
|
||||
void HttpClient::onSendRequest(const HttpRequest &request)
|
||||
QFuture<QByteArray> HttpClient::get(const QNetworkRequest &request)
|
||||
{
|
||||
QJsonDocument doc(request.payload);
|
||||
LOG_MESSAGE(QString("HttpClient: data: %1").arg(doc.toJson(QJsonDocument::Indented)));
|
||||
LOG_MESSAGE(QString("HttpClient: GET %1").arg(request.url().toString()));
|
||||
|
||||
QNetworkReply *reply
|
||||
= m_manager->post(request.networkRequest, doc.toJson(QJsonDocument::Compact));
|
||||
addActiveRequest(reply, request.requestId);
|
||||
auto promise = std::make_shared<QPromise<QByteArray>>();
|
||||
promise->start();
|
||||
|
||||
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::finished, this, &HttpClient::onFinished);
|
||||
}
|
||||
|
||||
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;
|
||||
connect(reply, &QNetworkReply::finished, this, &HttpClient::onStreamingFinished);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* Copyright (C) 2025 Petr Mironychev
|
||||
*
|
||||
* This file is part of QodeAssist.
|
||||
@@ -19,24 +19,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <QHash>
|
||||
#include <QJsonObject>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QPromise>
|
||||
|
||||
namespace QodeAssist::LLMCore {
|
||||
|
||||
struct HttpRequest
|
||||
{
|
||||
QNetworkRequest networkRequest;
|
||||
QString requestId;
|
||||
QJsonObject payload;
|
||||
};
|
||||
|
||||
class HttpClient : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -45,21 +40,33 @@ public:
|
||||
HttpClient(QObject *parent = nullptr);
|
||||
~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);
|
||||
|
||||
signals:
|
||||
void sendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
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:
|
||||
void onSendRequest(const QodeAssist::LLMCore::HttpRequest &request);
|
||||
void onReadyRead();
|
||||
void onFinished();
|
||||
void onStreamingFinished();
|
||||
|
||||
private:
|
||||
QString addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
||||
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody, const QString &networkErrorString);
|
||||
void setupNonStreamingReply(QNetworkReply *reply, std::shared_ptr<QPromise<QByteArray>> promise);
|
||||
|
||||
QString findRequestId(QNetworkReply *reply);
|
||||
void addActiveRequest(QNetworkReply *reply, const QString &requestId);
|
||||
QString parseErrorFromResponse(int statusCode, const QByteArray &responseBody,
|
||||
const QString &networkErrorString);
|
||||
|
||||
QNetworkAccessManager *m_manager;
|
||||
QHash<QString, QNetworkReply *> m_activeRequests;
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include <QFuture>
|
||||
#include <utils/environment.h>
|
||||
#include <QNetworkRequest>
|
||||
#include <QObject>
|
||||
@@ -57,7 +60,7 @@ public:
|
||||
bool isToolsEnabled,
|
||||
bool isThinkingEnabled)
|
||||
= 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 QString apiKey() const = 0;
|
||||
virtual void prepareNetworkRequest(QNetworkRequest &networkRequest) const = 0;
|
||||
@@ -81,7 +84,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data)
|
||||
= 0;
|
||||
virtual void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
= 0;
|
||||
|
||||
signals:
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
|
||||
#include "ClaudeProvider.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QUrlQuery>
|
||||
|
||||
#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");
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("limit", "1000");
|
||||
@@ -160,32 +155,24 @@ QList<QString> ClaudeProvider::getInstalledModels(const QString &baseUrl)
|
||||
request.setRawHeader("x-api-key", apiKey().toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
|
||||
if (jsonObject.contains("data")) {
|
||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
if (modelObject.contains("id")) {
|
||||
QString modelId = modelObject["id"].toString();
|
||||
models.append(modelId);
|
||||
models.append(modelObject["id"].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching Claude models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> ClaudeProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||
@@ -240,12 +227,9 @@ void ClaudeProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
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
|
||||
@@ -289,11 +273,11 @@ void ClaudeProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void ClaudeProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("ClaudeProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -65,8 +65,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -19,11 +19,9 @@
|
||||
|
||||
#include "GoogleAIProvider.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QtCore/qurlquery.h>
|
||||
|
||||
#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()));
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
|
||||
if (jsonObject.contains("models")) {
|
||||
QJsonArray modelArray = jsonObject["models"].toArray();
|
||||
models.clear();
|
||||
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
if (modelObject.contains("name")) {
|
||||
@@ -190,12 +176,11 @@ QList<QString> GoogleAIProvider::getInstalledModels(const QString &url)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching Google AI models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> GoogleAIProvider::validateRequest(
|
||||
@@ -254,13 +239,10 @@ void GoogleAIProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("GoogleAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool GoogleAIProvider::supportsTools() const
|
||||
@@ -327,11 +309,11 @@ void GoogleAIProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void GoogleAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("GoogleAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -62,8 +62,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -27,11 +27,9 @@
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
@@ -71,35 +69,24 @@ bool LMStudioProvider::supportsModelListing() const
|
||||
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"));
|
||||
|
||||
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;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
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);
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
models.append(modelObject["id"].toString());
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching LMStudio models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> LMStudioProvider::validateRequest(
|
||||
@@ -149,13 +136,10 @@ void LMStudioProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("LMStudioProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool LMStudioProvider::supportsTools() const
|
||||
@@ -195,11 +179,11 @@ void LMStudioProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void LMStudioProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("LMStudioProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -61,8 +61,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -26,11 +26,9 @@
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
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(
|
||||
@@ -192,13 +190,10 @@ void LlamaCppProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("LlamaCppProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool LlamaCppProvider::supportsTools() const
|
||||
@@ -250,11 +245,11 @@ void LlamaCppProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void LlamaCppProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("LlamaCppProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -61,8 +61,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -27,11 +27,9 @@
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
@@ -71,43 +69,32 @@ bool MistralAIProvider::supportsModelListing() const
|
||||
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));
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
if (!apiKey().isEmpty()) {
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
|
||||
if (jsonObject.contains("data") && jsonObject["object"].toString() == "list") {
|
||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
if (modelObject.contains("id")) {
|
||||
QString modelId = modelObject["id"].toString();
|
||||
models.append(modelId);
|
||||
models.append(modelObject["id"].toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching Mistral AI models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> MistralAIProvider::validateRequest(
|
||||
@@ -170,13 +157,10 @@ void MistralAIProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("MistralAIProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool MistralAIProvider::supportsTools() const
|
||||
@@ -216,11 +200,11 @@ void MistralAIProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void MistralAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("MistralAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -61,8 +61,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -22,8 +22,6 @@
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
#include <QtCore/qeventloop.h>
|
||||
|
||||
#include "llmcore/ValidationUtils.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"));
|
||||
prepareNetworkRequest(request);
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
QJsonArray modelArray = jsonObject["models"].toArray();
|
||||
|
||||
for (const QJsonValue &value : modelArray) {
|
||||
QJsonObject modelObject = value.toObject();
|
||||
QString modelName = modelObject["name"].toString();
|
||||
models.append(modelName);
|
||||
models.append(modelObject["name"].toString());
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> OllamaProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||
@@ -248,12 +236,9 @@ void OllamaProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
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
|
||||
@@ -312,11 +297,11 @@ void OllamaProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void OllamaProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("OllamaProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -63,8 +63,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -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(
|
||||
@@ -178,13 +178,10 @@ void OpenAICompatProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
LOG_MESSAGE(
|
||||
QString("OpenAICompatProvider: Sending request %1 to %2").arg(requestId, url.toString()));
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool OpenAICompatProvider::supportsTools() const
|
||||
@@ -224,11 +221,11 @@ void OpenAICompatProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void OpenAICompatProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("OpenAICompatProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -61,8 +61,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -27,11 +27,9 @@
|
||||
#include "settings/GeneralSettings.hpp"
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
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));
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
if (!apiKey().isEmpty()) {
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray responseData = reply->readAll();
|
||||
QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
|
||||
if (jsonObject.contains("data")) {
|
||||
QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
@@ -176,12 +165,11 @@ QList<QString> OpenAIProvider::getInstalledModels(const QString &url)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> OpenAIProvider::validateRequest(const QJsonObject &request, LLMCore::TemplateType type)
|
||||
@@ -235,12 +223,9 @@ void OpenAIProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
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
|
||||
@@ -280,11 +265,11 @@ void OpenAIProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void OpenAIProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("OpenAIProvider request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -61,8 +61,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -28,11 +28,9 @@
|
||||
#include "settings/ProviderSettings.hpp"
|
||||
#include "settings/QuickRefactorSettings.hpp"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
namespace QodeAssist::Providers {
|
||||
|
||||
@@ -158,26 +156,17 @@ void OpenAIResponsesProvider::prepareRequest(
|
||||
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));
|
||||
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
if (!apiKey().isEmpty()) {
|
||||
request.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey()).toUtf8());
|
||||
}
|
||||
|
||||
QNetworkReply *reply = manager.get(request);
|
||||
QEventLoop loop;
|
||||
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
||||
loop.exec();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
const QByteArray responseData = reply->readAll();
|
||||
const QJsonDocument jsonResponse = QJsonDocument::fromJson(responseData);
|
||||
const QJsonObject jsonObject = jsonResponse.object();
|
||||
return httpClient()->get(request).then([](const QByteArray &data) {
|
||||
QList<QString> models;
|
||||
const QJsonObject jsonObject = QJsonDocument::fromJson(data).object();
|
||||
|
||||
if (jsonObject.contains("data")) {
|
||||
const QJsonArray modelArray = jsonObject["data"].toArray();
|
||||
@@ -200,12 +189,11 @@ QList<QString> OpenAIResponsesProvider::getInstalledModels(const QString &url)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(reply->errorString()));
|
||||
}
|
||||
|
||||
reply->deleteLater();
|
||||
return models;
|
||||
return models;
|
||||
}).onFailed([](const std::exception &e) {
|
||||
LOG_MESSAGE(QString("Error fetching OpenAI models: %1").arg(e.what()));
|
||||
return QList<QString>{};
|
||||
});
|
||||
}
|
||||
|
||||
QList<QString> OpenAIResponsesProvider::validateRequest(
|
||||
@@ -280,10 +268,7 @@ void OpenAIResponsesProvider::sendRequest(
|
||||
QNetworkRequest networkRequest(url);
|
||||
prepareNetworkRequest(networkRequest);
|
||||
|
||||
LLMCore::HttpRequest
|
||||
request{.networkRequest = networkRequest, .requestId = requestId, .payload = payload};
|
||||
|
||||
emit httpClient()->sendRequest(request);
|
||||
httpClient()->postStreaming(requestId, networkRequest, payload);
|
||||
}
|
||||
|
||||
bool OpenAIResponsesProvider::supportsTools() const
|
||||
@@ -344,11 +329,11 @@ void OpenAIResponsesProvider::onDataReceived(
|
||||
}
|
||||
|
||||
void OpenAIResponsesProvider::onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId, bool success, const QString &error)
|
||||
const QodeAssist::LLMCore::RequestID &requestId, std::optional<QString> error)
|
||||
{
|
||||
if (!success) {
|
||||
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, error));
|
||||
emit requestFailed(requestId, error);
|
||||
if (error) {
|
||||
LOG_MESSAGE(QString("OpenAIResponses request %1 failed: %2").arg(requestId, *error));
|
||||
emit requestFailed(requestId, *error);
|
||||
cleanupRequest(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
LLMCore::RequestType type,
|
||||
bool isToolsEnabled,
|
||||
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;
|
||||
QString apiKey() const override;
|
||||
void prepareNetworkRequest(QNetworkRequest &networkRequest) const override;
|
||||
@@ -62,8 +62,7 @@ public slots:
|
||||
const QodeAssist::LLMCore::RequestID &requestId, const QByteArray &data) override;
|
||||
void onRequestFinished(
|
||||
const QodeAssist::LLMCore::RequestID &requestId,
|
||||
bool success,
|
||||
const QString &error) override;
|
||||
std::optional<QString> error) override;
|
||||
|
||||
private slots:
|
||||
void onToolExecutionComplete(
|
||||
|
||||
@@ -236,7 +236,7 @@ public:
|
||||
closeChatViewAction.setText(Tr::tr("Close QodeAssist Chat"));
|
||||
closeChatViewAction.setIcon(QCODEASSIST_CHAT_ICON.icon());
|
||||
closeChatViewAction.addOnTriggered(this, [this] {
|
||||
if (m_chatView->isVisible()) {
|
||||
if (m_chatView && m_chatView->isActive() && m_chatView->isVisible()) {
|
||||
m_chatView->close();
|
||||
}
|
||||
});
|
||||
@@ -250,8 +250,6 @@ public:
|
||||
editorContextMenu->addAction(requestAction.command(), Core::Constants::G_DEFAULT_THREE);
|
||||
editorContextMenu->addAction(showChatViewAction.command(),
|
||||
Core::Constants::G_DEFAULT_THREE);
|
||||
editorContextMenu->addAction(closeChatViewAction.command(),
|
||||
Core::Constants::G_DEFAULT_THREE);
|
||||
}
|
||||
|
||||
Chat::ChatFileManager::cleanupGlobalIntermediateStorage();
|
||||
|
||||
@@ -29,21 +29,24 @@
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentRoleDialog::AgentRoleDialog(QWidget *parent)
|
||||
: QDialog(parent)
|
||||
, m_editMode(false)
|
||||
AgentRoleDialog::AgentRoleDialog(Action action, QWidget *parent)
|
||||
: QDialog{parent}
|
||||
, m_action{action}
|
||||
{
|
||||
setWindowTitle(tr("Add Agent Role"));
|
||||
setupUI();
|
||||
}
|
||||
auto getTitle = [](Action action) {
|
||||
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)
|
||||
: QDialog(parent)
|
||||
, m_editMode(editMode)
|
||||
{
|
||||
setWindowTitle(editMode ? tr("Edit Agent Role") : tr("Duplicate Agent Role"));
|
||||
setWindowTitle(getTitle(action));
|
||||
setupUI();
|
||||
setRole(role);
|
||||
}
|
||||
|
||||
void AgentRoleDialog::setupUI()
|
||||
@@ -83,7 +86,7 @@ void AgentRoleDialog::setupUI()
|
||||
connect(m_idEdit, &QLineEdit::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->setToolTip(tr("ID cannot be changed for existing roles"));
|
||||
}
|
||||
|
||||
@@ -34,8 +34,18 @@ class AgentRoleDialog : public QDialog
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRoleDialog(QWidget *parent = nullptr);
|
||||
explicit AgentRoleDialog(const AgentRole &role, bool editMode = true, QWidget *parent = nullptr);
|
||||
enum class Action {
|
||||
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;
|
||||
void setRole(const AgentRole &role);
|
||||
@@ -49,7 +59,7 @@ private:
|
||||
QTextEdit *m_descriptionEdit = nullptr;
|
||||
QTextEdit *m_systemPromptEdit = nullptr;
|
||||
QDialogButtonBox *m_buttonBox = nullptr;
|
||||
bool m_editMode = false;
|
||||
Action m_action;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -34,13 +34,6 @@
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
AgentRolesWidget::AgentRolesWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUI();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
void AgentRolesWidget::setupUI()
|
||||
{
|
||||
auto *mainLayout = new QVBoxLayout(this);
|
||||
@@ -129,7 +122,7 @@ void AgentRolesWidget::updateButtons()
|
||||
|
||||
void AgentRolesWidget::onAddRole()
|
||||
{
|
||||
AgentRoleDialog dialog(this);
|
||||
AgentRoleDialog dialog{AgentRoleDialog::Action::Add, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
@@ -170,7 +163,7 @@ void AgentRolesWidget::onEditRole()
|
||||
return;
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog(role, this);
|
||||
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Edit, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
@@ -203,7 +196,7 @@ void AgentRolesWidget::onDuplicateRole()
|
||||
role.id = baseId + QString::number(counter++);
|
||||
}
|
||||
|
||||
AgentRoleDialog dialog(role, false, this);
|
||||
AgentRoleDialog dialog{role, AgentRoleDialog::Action::Duplicate, this};
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
|
||||
@@ -19,19 +19,23 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <coreplugin/dialogs/ioptionspage.h>
|
||||
|
||||
class QListWidget;
|
||||
class QPushButton;
|
||||
|
||||
namespace QodeAssist::Settings {
|
||||
|
||||
class AgentRolesWidget : public QWidget
|
||||
class AgentRolesWidget : public Core::IOptionsPageWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AgentRolesWidget(QWidget *parent = nullptr);
|
||||
explicit AgentRolesWidget()
|
||||
{
|
||||
setupUI();
|
||||
loadRoles();
|
||||
}
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
@@ -43,6 +43,106 @@ ConfigurationManager &ConfigurationManager::instance()
|
||||
return instance;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> ConfigurationManager::getPredefinedConfigurations(
|
||||
ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> presets;
|
||||
|
||||
AIConfiguration claudeOpus;
|
||||
claudeOpus.id = "preset_claude_opus";
|
||||
claudeOpus.name = "Claude Opus 4.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
|
||||
{
|
||||
switch (type) {
|
||||
@@ -94,6 +194,9 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
|
||||
|
||||
configs->clear();
|
||||
|
||||
QVector<AIConfiguration> predefinedConfigs = getPredefinedConfigurations(type);
|
||||
configs->append(predefinedConfigs);
|
||||
|
||||
if (!ensureDirectoryExists(type)) {
|
||||
LOG_MESSAGE("Failed to create configuration directory");
|
||||
return false;
|
||||
@@ -131,6 +234,7 @@ bool ConfigurationManager::loadConfigurations(ConfigurationType type)
|
||||
config.customEndpoint = obj["customEndpoint"].toString();
|
||||
config.type = type;
|
||||
config.formatVersion = obj.value("formatVersion").toInt(1);
|
||||
config.isPredefined = false;
|
||||
|
||||
if (config.id.isEmpty() || config.name.isEmpty()) {
|
||||
LOG_MESSAGE(QString("Invalid configuration data in file: %1").arg(fileInfo.fileName()));
|
||||
@@ -185,6 +289,12 @@ bool ConfigurationManager::saveConfiguration(const AIConfiguration &config)
|
||||
|
||||
bool ConfigurationManager::deleteConfiguration(const QString &id, ConfigurationType type)
|
||||
{
|
||||
AIConfiguration config = getConfigurationById(id, type);
|
||||
if (config.isPredefined) {
|
||||
LOG_MESSAGE(QString("Cannot delete predefined configuration: %1").arg(id));
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir dir(getConfigurationDirectory(type));
|
||||
QStringList filters;
|
||||
filters << QString("*_%1.json").arg(id);
|
||||
|
||||
@@ -41,6 +41,7 @@ struct AIConfiguration
|
||||
QString customEndpoint;
|
||||
ConfigurationType type;
|
||||
int formatVersion = CONFIGURATION_FORMAT_VERSION;
|
||||
bool isPredefined = false;
|
||||
};
|
||||
|
||||
class ConfigurationManager : public QObject
|
||||
@@ -58,6 +59,8 @@ public:
|
||||
AIConfiguration getConfigurationById(const QString &id, ConfigurationType type) const;
|
||||
|
||||
QString getConfigurationDirectory(ConfigurationType type) const;
|
||||
|
||||
static QVector<AIConfiguration> getPredefinedConfigurations(ConfigurationType type);
|
||||
|
||||
signals:
|
||||
void configurationsChanged(ConfigurationType type);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <coreplugin/icore.h>
|
||||
#include <utils/layoutbuilder.h>
|
||||
#include <utils/utilsicons.h>
|
||||
#include <QComboBox>
|
||||
#include <QDesktopServices>
|
||||
#include <QDir>
|
||||
#include <QInputDialog>
|
||||
@@ -88,6 +89,27 @@ GeneralSettings::GeneralSettings()
|
||||
|
||||
resetToDefaults.m_buttonText = TrConstants::RESET_TO_DEFAULTS;
|
||||
checkUpdate.m_buttonText = TrConstants::CHECK_UPDATE;
|
||||
|
||||
ccPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
ccPresetConfig.setLabelText(Tr::tr("Quick Setup"));
|
||||
loadPresetConfigurations(ccPresetConfig, ConfigurationType::CodeCompletion);
|
||||
|
||||
ccConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
|
||||
ccConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
|
||||
|
||||
caPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
caPresetConfig.setLabelText(Tr::tr("Quick Setup"));
|
||||
loadPresetConfigurations(caPresetConfig, ConfigurationType::Chat);
|
||||
|
||||
caConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
|
||||
caConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
|
||||
|
||||
qrPresetConfig.setDisplayStyle(Utils::SelectionAspect::DisplayStyle::ComboBox);
|
||||
qrPresetConfig.setLabelText(Tr::tr("Quick Setup"));
|
||||
loadPresetConfigurations(qrPresetConfig, ConfigurationType::QuickRefactor);
|
||||
|
||||
qrConfigureApiKey.m_buttonText = Tr::tr("Configure API Key");
|
||||
qrConfigureApiKey.m_tooltip = Tr::tr("Open Provider Settings to configure API keys");
|
||||
|
||||
initStringAspect(ccProvider, Constants::CC_PROVIDER, TrConstants::PROVIDER, "Ollama");
|
||||
ccProvider.setReadOnly(true);
|
||||
@@ -127,6 +149,7 @@ GeneralSettings::GeneralSettings()
|
||||
|
||||
ccSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
|
||||
ccLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
|
||||
ccLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
|
||||
ccOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
|
||||
ccOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
|
||||
ccOpenConfigFolder.m_isCompact = true;
|
||||
@@ -218,6 +241,7 @@ GeneralSettings::GeneralSettings()
|
||||
|
||||
caSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
|
||||
caLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
|
||||
caLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
|
||||
caOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
|
||||
caOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
|
||||
caOpenConfigFolder.m_isCompact = true;
|
||||
@@ -262,6 +286,7 @@ GeneralSettings::GeneralSettings()
|
||||
|
||||
qrSaveConfig.m_buttonText = TrConstants::SAVE_CONFIG;
|
||||
qrLoadConfig.m_buttonText = TrConstants::LOAD_CONFIG;
|
||||
qrLoadConfig.m_tooltip = Tr::tr("Load configuration (includes predefined cloud models)");
|
||||
qrOpenConfigFolder.m_buttonText = TrConstants::OPEN_CONFIG_FOLDER;
|
||||
qrOpenConfigFolder.m_icon = Utils::Icons::OPENFILE.icon();
|
||||
qrOpenConfigFolder.m_isCompact = true;
|
||||
@@ -325,17 +350,24 @@ GeneralSettings::GeneralSettings()
|
||||
title(TrConstants::CODE_COMPLETION),
|
||||
Column{
|
||||
Row{ccSaveConfig, ccLoadConfig, ccOpenConfigFolder, Stretch{1}},
|
||||
Row{ccPresetConfig, ccConfigureApiKey, Stretch{1}},
|
||||
ccGrid,
|
||||
Row{specifyPreset1, preset1Language, Stretch{1}},
|
||||
ccPreset1Grid}};
|
||||
|
||||
auto caGroup = Group{
|
||||
title(TrConstants::CHAT_ASSISTANT),
|
||||
Column{Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}}, caGrid}};
|
||||
Column{
|
||||
Row{caSaveConfig, caLoadConfig, caOpenConfigFolder, Stretch{1}},
|
||||
Row{caPresetConfig, caConfigureApiKey, Stretch{1}},
|
||||
caGrid}};
|
||||
|
||||
auto qrGroup = Group{
|
||||
title(TrConstants::QUICK_REFACTOR),
|
||||
Column{Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}}, qrGrid}};
|
||||
Column{
|
||||
Row{qrSaveConfig, qrLoadConfig, qrOpenConfigFolder, Stretch{1}},
|
||||
Row{qrPresetConfig, qrConfigureApiKey, Stretch{1}},
|
||||
qrGrid}};
|
||||
|
||||
auto rootLayout = Column{
|
||||
Row{enableQodeAssist, Stretch{1}, Row{checkUpdate, resetToDefaults}},
|
||||
@@ -420,7 +452,7 @@ void GeneralSettings::showModelsNotFoundDialog(Utils::StringAspect &aspect)
|
||||
|
||||
connect(configureApiKeyBtn, &QPushButton::clicked, &dialog, [&dialog]() {
|
||||
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);
|
||||
@@ -570,6 +602,33 @@ void GeneralSettings::setupConnections()
|
||||
connect(&checkUpdate, &ButtonAspect::clicked, this, [this]() {
|
||||
QodeAssist::UpdateDialog::checkForUpdatesAndShow(Core::ICore::dialogParent());
|
||||
});
|
||||
|
||||
connect(&ccPresetConfig, &Utils::SelectionAspect::volatileValueChanged, this, [this]() {
|
||||
applyPresetConfiguration(ccPresetConfig.volatileValue(), ConfigurationType::CodeCompletion);
|
||||
ccPresetConfig.setValue(0);
|
||||
});
|
||||
|
||||
connect(&ccConfigureApiKey, &ButtonAspect::clicked, this, []() {
|
||||
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]() {
|
||||
updatePreset1Visiblity(specifyPreset1.volatileValue());
|
||||
@@ -776,11 +835,33 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
|
||||
|
||||
SettingsDialog dialog(TrConstants::LOAD_CONFIGURATION);
|
||||
dialog.addLabel(TrConstants::SELECT_CONFIGURATION);
|
||||
|
||||
int predefinedCount = 0;
|
||||
for (const AIConfiguration &config : configs) {
|
||||
if (config.isPredefined) {
|
||||
predefinedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (predefinedCount > 0) {
|
||||
auto *hintLabel = dialog.addLabel(
|
||||
Tr::tr("[Preset] configurations are predefined cloud models ready to use."));
|
||||
QFont hintFont = hintLabel->font();
|
||||
hintFont.setItalic(true);
|
||||
hintFont.setPointSize(hintFont.pointSize() - 1);
|
||||
hintLabel->setFont(hintFont);
|
||||
hintLabel->setStyleSheet("color: gray;");
|
||||
}
|
||||
|
||||
dialog.addSpacing();
|
||||
|
||||
QStringList configNames;
|
||||
for (const AIConfiguration &config : configs) {
|
||||
configNames.append(config.name);
|
||||
QString displayName = config.name;
|
||||
if (config.isPredefined) {
|
||||
displayName = QString("[Preset] %1").arg(config.name);
|
||||
}
|
||||
configNames.append(displayName);
|
||||
}
|
||||
|
||||
auto configList = dialog.addComboBox(configNames, QString());
|
||||
@@ -790,9 +871,31 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
|
||||
auto *okButton = new QPushButton(TrConstants::OK);
|
||||
auto *cancelButton = new QPushButton(TrConstants::CANCEL);
|
||||
|
||||
auto updateDeleteButtonState = [&]() {
|
||||
int currentIndex = configList->currentIndex();
|
||||
if (currentIndex >= 0 && currentIndex < configs.size()) {
|
||||
deleteButton->setEnabled(!configs[currentIndex].isPredefined);
|
||||
}
|
||||
};
|
||||
|
||||
connect(configList,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
updateDeleteButtonState);
|
||||
|
||||
updateDeleteButtonState();
|
||||
|
||||
connect(deleteButton, &QPushButton::clicked, &dialog, [&]() {
|
||||
int currentIndex = configList->currentIndex();
|
||||
if (currentIndex >= 0 && currentIndex < configs.size()) {
|
||||
const AIConfiguration &configToDelete = configs[currentIndex];
|
||||
if (configToDelete.isPredefined) {
|
||||
QMessageBox::information(
|
||||
&dialog,
|
||||
TrConstants::DELETE_CONFIGURATION,
|
||||
Tr::tr("Predefined configurations cannot be deleted."));
|
||||
return;
|
||||
}
|
||||
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
&dialog,
|
||||
TrConstants::DELETE_CONFIGURATION,
|
||||
@@ -800,7 +903,6 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
|
||||
QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
const AIConfiguration &configToDelete = configs[currentIndex];
|
||||
if (manager.deleteConfiguration(configToDelete.id, type)) {
|
||||
dialog.accept();
|
||||
onLoadConfiguration(prefix);
|
||||
@@ -860,6 +962,73 @@ void GeneralSettings::onLoadConfiguration(const QString &prefix)
|
||||
dialog.exec();
|
||||
}
|
||||
|
||||
void GeneralSettings::loadPresetConfigurations(Utils::SelectionAspect &aspect,
|
||||
ConfigurationType type)
|
||||
{
|
||||
QVector<AIConfiguration> presets = ConfigurationManager::getPredefinedConfigurations(type);
|
||||
|
||||
if (type == ConfigurationType::CodeCompletion) {
|
||||
m_ccPresets = presets;
|
||||
} else if (type == ConfigurationType::Chat) {
|
||||
m_caPresets = presets;
|
||||
} else if (type == ConfigurationType::QuickRefactor) {
|
||||
m_qrPresets = presets;
|
||||
}
|
||||
|
||||
aspect.addOption(Tr::tr("-- Select Preset --"));
|
||||
for (const AIConfiguration &config : presets) {
|
||||
aspect.addOption(config.name);
|
||||
}
|
||||
aspect.setDefaultValue(0);
|
||||
}
|
||||
|
||||
void GeneralSettings::applyPresetConfiguration(int index, ConfigurationType type)
|
||||
{
|
||||
if (index <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
QVector<AIConfiguration> *presets = nullptr;
|
||||
if (type == ConfigurationType::CodeCompletion) {
|
||||
presets = &m_ccPresets;
|
||||
} else if (type == ConfigurationType::Chat) {
|
||||
presets = &m_caPresets;
|
||||
} else if (type == ConfigurationType::QuickRefactor) {
|
||||
presets = &m_qrPresets;
|
||||
}
|
||||
|
||||
if (!presets || index - 1 >= presets->size()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AIConfiguration &config = presets->at(index - 1);
|
||||
|
||||
if (type == ConfigurationType::CodeCompletion) {
|
||||
ccProvider.setValue(config.provider);
|
||||
ccModel.setValue(config.model);
|
||||
ccTemplate.setValue(config.templateName);
|
||||
ccUrl.setValue(config.url);
|
||||
ccEndpointMode.setValue(ccEndpointMode.indexForDisplay(config.endpointMode));
|
||||
ccCustomEndpoint.setValue(config.customEndpoint);
|
||||
} else if (type == ConfigurationType::Chat) {
|
||||
caProvider.setValue(config.provider);
|
||||
caModel.setValue(config.model);
|
||||
caTemplate.setValue(config.templateName);
|
||||
caUrl.setValue(config.url);
|
||||
caEndpointMode.setValue(caEndpointMode.indexForDisplay(config.endpointMode));
|
||||
caCustomEndpoint.setValue(config.customEndpoint);
|
||||
} else if (type == ConfigurationType::QuickRefactor) {
|
||||
qrProvider.setValue(config.provider);
|
||||
qrModel.setValue(config.model);
|
||||
qrTemplate.setValue(config.templateName);
|
||||
qrUrl.setValue(config.url);
|
||||
qrEndpointMode.setValue(qrEndpointMode.indexForDisplay(config.endpointMode));
|
||||
qrCustomEndpoint.setValue(config.customEndpoint);
|
||||
}
|
||||
|
||||
writeSettings();
|
||||
}
|
||||
|
||||
class GeneralSettingsPage : public Core::IOptionsPage
|
||||
{
|
||||
public:
|
||||
@@ -877,5 +1046,29 @@ public:
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <QPointer>
|
||||
|
||||
#include "ButtonAspect.hpp"
|
||||
#include "ConfigurationManager.hpp"
|
||||
|
||||
namespace Utils {
|
||||
class DetailsWidget;
|
||||
@@ -46,6 +47,9 @@ public:
|
||||
ButtonAspect resetToDefaults{this};
|
||||
|
||||
// code completion setttings
|
||||
Utils::SelectionAspect ccPresetConfig{this};
|
||||
ButtonAspect ccConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect ccProvider{this};
|
||||
ButtonAspect ccSelectProvider{this};
|
||||
|
||||
@@ -91,6 +95,9 @@ public:
|
||||
ButtonAspect ccPreset1SelectTemplate{this};
|
||||
|
||||
// chat assistant settings
|
||||
Utils::SelectionAspect caPresetConfig{this};
|
||||
ButtonAspect caConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect caProvider{this};
|
||||
ButtonAspect caSelectProvider{this};
|
||||
|
||||
@@ -116,6 +123,9 @@ public:
|
||||
ButtonAspect caOpenConfigFolder{this};
|
||||
|
||||
// quick refactor settings
|
||||
Utils::SelectionAspect qrPresetConfig{this};
|
||||
ButtonAspect qrConfigureApiKey{this};
|
||||
|
||||
Utils::StringAspect qrProvider{this};
|
||||
ButtonAspect qrSelectProvider{this};
|
||||
|
||||
@@ -162,12 +172,22 @@ public:
|
||||
|
||||
void onSaveConfiguration(const QString &prefix);
|
||||
void onLoadConfiguration(const QString &prefix);
|
||||
|
||||
void loadPresetConfigurations(Utils::SelectionAspect &aspect, ConfigurationType type);
|
||||
void applyPresetConfiguration(int index, ConfigurationType type);
|
||||
|
||||
private:
|
||||
void setupConnections();
|
||||
void resetPageToDefaults();
|
||||
|
||||
QVector<AIConfiguration> m_ccPresets;
|
||||
QVector<AIConfiguration> m_caPresets;
|
||||
QVector<AIConfiguration> m_qrPresets;
|
||||
};
|
||||
|
||||
GeneralSettings &generalSettings();
|
||||
|
||||
void showSettings(const Utils::Id page);
|
||||
void showSettings(const Utils::Id page, Utils::Id item);
|
||||
|
||||
} // namespace QodeAssist::Settings
|
||||
|
||||
@@ -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_MACOS[] = "QodeAssist.caAllowedTerminalCommandsMacOS";
|
||||
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_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
|
||||
|
||||
@@ -128,6 +128,14 @@ ToolsSettings::ToolsSettings()
|
||||
allowedTerminalCommandsWindows.setDisplayStyle(Utils::StringAspect::LineEditDisplay);
|
||||
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");
|
||||
|
||||
readSettings();
|
||||
@@ -167,6 +175,7 @@ ToolsSettings::ToolsSettings()
|
||||
enableTerminalCommandTool,
|
||||
enableTodoTool,
|
||||
currentOsCommands,
|
||||
terminalCommandTimeout,
|
||||
autoApplyFileEdits}},
|
||||
Stretch{1}};
|
||||
});
|
||||
@@ -203,6 +212,7 @@ void ToolsSettings::resetSettingsToDefaults()
|
||||
resetAspect(allowedTerminalCommandsLinux);
|
||||
resetAspect(allowedTerminalCommandsMacOS);
|
||||
resetAspect(allowedTerminalCommandsWindows);
|
||||
resetAspect(terminalCommandTimeout);
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public:
|
||||
Utils::StringAspect allowedTerminalCommandsLinux{this};
|
||||
Utils::StringAspect allowedTerminalCommandsMacOS{this};
|
||||
Utils::StringAspect allowedTerminalCommandsWindows{this};
|
||||
Utils::IntegerAspect terminalCommandTimeout{this};
|
||||
Utils::BoolAspect autoApplyFileEdits{this};
|
||||
|
||||
private:
|
||||
|
||||
@@ -80,7 +80,10 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<QString> getInstalledModels(const QString &url) override { return {}; }
|
||||
QFuture<QList<QString>> getInstalledModels(const QString &) override
|
||||
{
|
||||
return QtFuture::makeReadyFuture(QList<QString>{});
|
||||
}
|
||||
|
||||
QStringList validateRequest(
|
||||
const QJsonObject &request, LLMCore::TemplateType templateType) override
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
#include <QSharedPointer>
|
||||
#include <QTimer>
|
||||
|
||||
#include <atomic>
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
|
||||
ExecuteTerminalCommandTool::ExecuteTerminalCommandTool(QObject *parent)
|
||||
@@ -188,54 +190,66 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
QFuture<QString> future = promise->future();
|
||||
promise->start();
|
||||
|
||||
auto resolved = std::make_shared<std::atomic<bool>>(false);
|
||||
|
||||
QProcess *process = new QProcess();
|
||||
process->setWorkingDirectory(workingDir);
|
||||
process->setProcessChannelMode(QProcess::MergedChannels);
|
||||
|
||||
|
||||
process->setReadChannel(QProcess::StandardOutput);
|
||||
|
||||
const int timeoutMs = commandTimeoutMs();
|
||||
|
||||
QTimer *timeoutTimer = new QTimer();
|
||||
timeoutTimer->setSingleShot(true);
|
||||
timeoutTimer->setInterval(COMMAND_TIMEOUT_MS);
|
||||
|
||||
auto outputSize = QSharedPointer<qint64>::create(0);
|
||||
timeoutTimer->setInterval(timeoutMs);
|
||||
|
||||
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")
|
||||
.arg(command)
|
||||
.arg(args)
|
||||
.arg(COMMAND_TIMEOUT_MS));
|
||||
|
||||
.arg(timeoutMs));
|
||||
|
||||
process->terminate();
|
||||
|
||||
|
||||
QTimer::singleShot(1000, process, [process]() {
|
||||
if (process->state() == QProcess::Running) {
|
||||
if (process->state() != QProcess::NotRunning) {
|
||||
LOG_MESSAGE("ExecuteTerminalCommandTool: Forcefully killing process after timeout");
|
||||
process->kill();
|
||||
}
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
|
||||
promise->addResult(QString("Error: Command '%1 %2' timed out after %3 seconds. "
|
||||
"The process has been terminated.")
|
||||
.arg(command)
|
||||
.arg(args.isEmpty() ? "" : args)
|
||||
.arg(COMMAND_TIMEOUT_MS / 1000));
|
||||
.arg(timeoutMs / 1000));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
timeoutTimer->deleteLater();
|
||||
});
|
||||
|
||||
QObject::connect(
|
||||
process,
|
||||
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) {
|
||||
if (*resolved) {
|
||||
process->deleteLater();
|
||||
return;
|
||||
}
|
||||
*resolved = true;
|
||||
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
|
||||
const QByteArray rawOutput = process->readAll();
|
||||
*outputSize += rawOutput.size();
|
||||
const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), *outputSize);
|
||||
const qint64 outputSize = rawOutput.size();
|
||||
const QString output = sanitizeOutput(QString::fromUtf8(rawOutput), outputSize);
|
||||
|
||||
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 "
|
||||
"successfully (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(*outputSize));
|
||||
.arg(outputSize));
|
||||
promise->addResult(
|
||||
QString("Command '%1' executed successfully.\n\nOutput:\n%2")
|
||||
.arg(fullCommand)
|
||||
@@ -254,7 +268,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
"exit code %2 (output size: %3 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(exitCode)
|
||||
.arg(*outputSize));
|
||||
.arg(outputSize));
|
||||
promise->addResult(
|
||||
QString("Command '%1' failed with exit code %2.\n\nOutput:\n%3")
|
||||
.arg(fullCommand)
|
||||
@@ -265,7 +279,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Command '%1' crashed or was "
|
||||
"terminated (output size: %2 bytes)")
|
||||
.arg(fullCommand)
|
||||
.arg(*outputSize));
|
||||
.arg(outputSize));
|
||||
const QString error = process->errorString();
|
||||
promise->addResult(
|
||||
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();
|
||||
});
|
||||
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, command, args, timeoutTimer](
|
||||
QObject::connect(process, &QProcess::errorOccurred, [process, promise, resolved, command, args, timeoutTimer](
|
||||
QProcess::ProcessError error) {
|
||||
if (promise->future().isFinished()) {
|
||||
if (*resolved) {
|
||||
process->deleteLater();
|
||||
return;
|
||||
}
|
||||
*resolved = true;
|
||||
|
||||
timeoutTimer->stop();
|
||||
timeoutTimer->deleteLater();
|
||||
@@ -292,7 +308,7 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
.arg(fullCommand)
|
||||
.arg(error)
|
||||
.arg(process->errorString()));
|
||||
|
||||
|
||||
QString errorMessage;
|
||||
switch (error) {
|
||||
case QProcess::FailedToStart:
|
||||
@@ -318,71 +334,46 @@ QFuture<QString> ExecuteTerminalCommandTool::executeAsync(const QJsonObject &inp
|
||||
.arg(process->errorString());
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
promise->addResult(QString("Error: %1").arg(errorMessage));
|
||||
promise->finish();
|
||||
process->deleteLater();
|
||||
});
|
||||
|
||||
QString fullCommand = command;
|
||||
QStringList argsList;
|
||||
if (!args.isEmpty()) {
|
||||
fullCommand += " " + args;
|
||||
argsList = QProcess::splitCommand(args);
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
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",
|
||||
"set", "path", "prompt", "ver", "vol", "date", "time"
|
||||
};
|
||||
|
||||
|
||||
const QString lowerCommand = command.toLower();
|
||||
const bool isBuiltin = windowsBuiltinCommands.contains(lowerCommand);
|
||||
|
||||
|
||||
if (isBuiltin) {
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Executing Windows builtin command '%1' via cmd.exe")
|
||||
.arg(command));
|
||||
process->start("cmd.exe", QStringList() << "/c" << fullCommand);
|
||||
QStringList cmdArgs;
|
||||
cmdArgs << "/c" << command;
|
||||
cmdArgs.append(argsList);
|
||||
process->start("cmd.exe", cmdArgs);
|
||||
} else {
|
||||
#endif
|
||||
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
|
||||
process->start(command, argsList);
|
||||
}
|
||||
#else
|
||||
process->start(command, argsList);
|
||||
#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();
|
||||
|
||||
LOG_MESSAGE(QString("ExecuteTerminalCommandTool: Process start requested for '%1'")
|
||||
.arg(command));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@@ -414,19 +405,27 @@ bool ExecuteTerminalCommandTool::areArgumentsSafe(const QString &args) const
|
||||
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 = {
|
||||
";", // Command separator
|
||||
"&&", // AND operator
|
||||
"||", // OR operator
|
||||
"&", // Command separator / background execution
|
||||
"|", // Pipe operator
|
||||
">", // Output redirection
|
||||
">>", // Append redirection
|
||||
"<", // Input redirection
|
||||
"`", // Command substitution
|
||||
"$(", // Command substitution
|
||||
"$()", // Command substitution
|
||||
"\\n", // Newline (could start new command)
|
||||
"\\r" // Carriage return
|
||||
"${", // Variable expansion
|
||||
"\n", // Newline (could start new command)
|
||||
"\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) {
|
||||
@@ -456,9 +455,6 @@ QString ExecuteTerminalCommandTool::sanitizeOutput(const QString &output, qint64
|
||||
|
||||
QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
|
||||
{
|
||||
static QString cachedCommandsStr;
|
||||
static QStringList cachedCommands;
|
||||
|
||||
QString commandsStr;
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
@@ -471,28 +467,27 @@ QStringList ExecuteTerminalCommandTool::getAllowedCommands() const
|
||||
commandsStr = Settings::toolsSettings().allowedTerminalCommandsLinux().trimmed(); // fallback
|
||||
#endif
|
||||
|
||||
if (commandsStr == cachedCommandsStr && !cachedCommands.isEmpty()) {
|
||||
return cachedCommands;
|
||||
}
|
||||
|
||||
cachedCommandsStr = commandsStr;
|
||||
cachedCommands.clear();
|
||||
|
||||
if (commandsStr.isEmpty()) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
QStringList result;
|
||||
const QStringList rawCommands = commandsStr.split(',', Qt::SkipEmptyParts);
|
||||
cachedCommands.reserve(rawCommands.size());
|
||||
|
||||
result.reserve(rawCommands.size());
|
||||
|
||||
for (const QString &cmd : rawCommands) {
|
||||
const QString trimmed = cmd.trimmed();
|
||||
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
|
||||
@@ -518,7 +513,7 @@ QString ExecuteTerminalCommandTool::getCommandDescription() const
|
||||
"Commands have a %2 second timeout. "
|
||||
"Returns the command output (stdout and stderr) or an error message if the command fails.%3")
|
||||
.arg(allowedList)
|
||||
.arg(COMMAND_TIMEOUT_MS / 1000)
|
||||
.arg(commandTimeoutMs() / 1000)
|
||||
.arg(osInfo);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@ private:
|
||||
QString getCommandDescription() const;
|
||||
QString sanitizeOutput(const QString &output, qint64 maxSize) const;
|
||||
|
||||
int commandTimeoutMs() const;
|
||||
|
||||
// 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 int MAX_COMMAND_LENGTH = 1024;
|
||||
static constexpr int MAX_ARGS_LENGTH = 4096;
|
||||
static constexpr int PROCESS_START_TIMEOUT_MS = 3000;
|
||||
};
|
||||
|
||||
} // namespace QodeAssist::Tools
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -33,7 +33,6 @@
|
||||
#include "GetIssuesListTool.hpp"
|
||||
#include "ListProjectFilesTool.hpp"
|
||||
#include "ProjectSearchTool.hpp"
|
||||
#include "ReadVisibleFilesTool.hpp"
|
||||
#include "TodoTool.hpp"
|
||||
|
||||
namespace QodeAssist::Tools {
|
||||
@@ -46,7 +45,6 @@ ToolsFactory::ToolsFactory(QObject *parent)
|
||||
|
||||
void ToolsFactory::registerTools()
|
||||
{
|
||||
registerTool(new ReadVisibleFilesTool(this));
|
||||
registerTool(new ListProjectFilesTool(this));
|
||||
registerTool(new GetIssuesListTool(this));
|
||||
registerTool(new CreateNewFileTool(this));
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFontMetrics>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QIcon>
|
||||
#include <QLabel>
|
||||
@@ -72,15 +73,15 @@ static QIcon createThemedIcon(const QString &svgPath, const QColor &color)
|
||||
painter.end();
|
||||
|
||||
QImage image = pixmap.toImage().convertToFormat(QImage::Format_ARGB32);
|
||||
|
||||
|
||||
uchar *bits = image.bits();
|
||||
const int bytesPerPixel = 4;
|
||||
const int totalBytes = image.width() * image.height() * bytesPerPixel;
|
||||
|
||||
|
||||
const int newR = color.red();
|
||||
const int newG = color.green();
|
||||
const int newB = color.blue();
|
||||
|
||||
|
||||
for (int i = 0; i < totalBytes; i += bytesPerPixel) {
|
||||
int alpha = bits[i + 3];
|
||||
if (alpha > 0) {
|
||||
@@ -100,11 +101,17 @@ QuickRefactorDialog::QuickRefactorDialog(QWidget *parent, const QString &lastIns
|
||||
setWindowTitle(Tr::tr("Quick Refactor"));
|
||||
setupUi();
|
||||
|
||||
if (!m_lastInstructions.isEmpty()) {
|
||||
m_instructionEdit->setPlainText(m_lastInstructions);
|
||||
m_instructionEdit->selectAll();
|
||||
}
|
||||
|
||||
QTimer::singleShot(0, this, &QuickRefactorDialog::updateDialogSize);
|
||||
m_textEdit->installEventFilter(this);
|
||||
m_instructionEdit->installEventFilter(this);
|
||||
m_commandsComboBox->installEventFilter(this);
|
||||
updateDialogSize();
|
||||
|
||||
m_commandsComboBox->setFocus();
|
||||
m_instructionEdit->setFocus();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::setupUi()
|
||||
@@ -173,56 +180,65 @@ void QuickRefactorDialog::setupUi()
|
||||
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
|
||||
QHBoxLayout *instructionsLayout = new QHBoxLayout();
|
||||
instructionsLayout->setSpacing(4);
|
||||
QLabel *instructionLabel = new QLabel(Tr::tr("Your Current Instruction:"), this);
|
||||
mainLayout->addWidget(instructionLabel);
|
||||
|
||||
QLabel *instructionsLabel = new QLabel(Tr::tr("Custom Instructions:"), this);
|
||||
instructionsLayout->addWidget(instructionsLabel);
|
||||
m_instructionEdit = new QPlainTextEdit(this);
|
||||
m_instructionEdit->setMinimumHeight(80);
|
||||
m_instructionEdit->setPlaceholderText(Tr::tr("Type or edit your instruction..."));
|
||||
mainLayout->addWidget(m_instructionEdit);
|
||||
|
||||
QHBoxLayout *savedInstructionsLayout = new QHBoxLayout();
|
||||
savedInstructionsLayout->setSpacing(4);
|
||||
|
||||
QLabel *savedLabel = new QLabel(Tr::tr("Or Load saved:"), this);
|
||||
savedInstructionsLayout->addWidget(savedLabel);
|
||||
|
||||
m_commandsComboBox = new QComboBox(this);
|
||||
m_commandsComboBox->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||
m_commandsComboBox->setEditable(true);
|
||||
m_commandsComboBox->setInsertPolicy(QComboBox::NoInsert);
|
||||
m_commandsComboBox->lineEdit()->setPlaceholderText("Search or select instruction...");
|
||||
|
||||
m_commandsComboBox->lineEdit()->setPlaceholderText(Tr::tr("Search saved instructions..."));
|
||||
|
||||
QCompleter *completer = new QCompleter(this);
|
||||
completer->setCompletionMode(QCompleter::PopupCompletion);
|
||||
completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
completer->setFilterMode(Qt::MatchContains);
|
||||
m_commandsComboBox->setCompleter(completer);
|
||||
|
||||
instructionsLayout->addWidget(m_commandsComboBox);
|
||||
|
||||
savedInstructionsLayout->addWidget(m_commandsComboBox);
|
||||
|
||||
m_addCommandButton = new QToolButton(this);
|
||||
m_addCommandButton->setText("+");
|
||||
m_addCommandButton->setToolTip(Tr::tr("Add Custom Instruction"));
|
||||
instructionsLayout->addWidget(m_addCommandButton);
|
||||
m_addCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_addCommandButton);
|
||||
|
||||
m_editCommandButton = new QToolButton(this);
|
||||
m_editCommandButton->setText("✎");
|
||||
m_editCommandButton->setToolTip(Tr::tr("Edit Custom Instruction"));
|
||||
instructionsLayout->addWidget(m_editCommandButton);
|
||||
m_editCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_editCommandButton);
|
||||
|
||||
m_deleteCommandButton = new QToolButton(this);
|
||||
m_deleteCommandButton->setText("−");
|
||||
m_deleteCommandButton->setToolTip(Tr::tr("Delete Custom Instruction"));
|
||||
instructionsLayout->addWidget(m_deleteCommandButton);
|
||||
m_deleteCommandButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_deleteCommandButton);
|
||||
|
||||
m_openFolderButton = new QToolButton(this);
|
||||
m_openFolderButton->setText("📁");
|
||||
m_openFolderButton->setToolTip(Tr::tr("Open Instructions Folder"));
|
||||
instructionsLayout->addWidget(m_openFolderButton);
|
||||
m_openFolderButton->setFocusPolicy(Qt::NoFocus);
|
||||
savedInstructionsLayout->addWidget(m_openFolderButton);
|
||||
|
||||
mainLayout->addLayout(instructionsLayout);
|
||||
mainLayout->addLayout(savedInstructionsLayout);
|
||||
|
||||
m_instructionsLabel = new QLabel(Tr::tr("Additional instructions (optional):"), this);
|
||||
mainLayout->addWidget(m_instructionsLabel);
|
||||
|
||||
m_textEdit = new QPlainTextEdit(this);
|
||||
m_textEdit->setMinimumHeight(100);
|
||||
m_textEdit->setPlaceholderText(Tr::tr("Add extra details or modifications to the selected instruction..."));
|
||||
|
||||
connect(m_textEdit, &QPlainTextEdit::textChanged, this, &QuickRefactorDialog::updateDialogSize);
|
||||
connect(
|
||||
m_instructionEdit,
|
||||
&QPlainTextEdit::textChanged,
|
||||
this,
|
||||
&QuickRefactorDialog::updateDialogSize);
|
||||
connect(
|
||||
m_commandsComboBox,
|
||||
QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
@@ -242,8 +258,6 @@ void QuickRefactorDialog::setupUi()
|
||||
this,
|
||||
&QuickRefactorDialog::onOpenInstructionsFolder);
|
||||
|
||||
mainLayout->addWidget(m_textEdit);
|
||||
|
||||
loadCustomCommands();
|
||||
loadAvailableConfigurations();
|
||||
|
||||
@@ -255,12 +269,23 @@ void QuickRefactorDialog::setupUi()
|
||||
|
||||
QDialogButtonBox *buttonBox
|
||||
= new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttonBox, &QDialogButtonBox::accepted, this, &QuickRefactorDialog::validateAndAccept);
|
||||
connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
mainLayout->addWidget(buttonBox);
|
||||
|
||||
QPushButton *okButton = buttonBox->button(QDialogButtonBox::Ok);
|
||||
QPushButton *cancelButton = buttonBox->button(QDialogButtonBox::Cancel);
|
||||
|
||||
setTabOrder(m_commandsComboBox, m_textEdit);
|
||||
setTabOrder(m_textEdit, buttonBox);
|
||||
if (okButton) {
|
||||
okButton->installEventFilter(this);
|
||||
}
|
||||
if (cancelButton) {
|
||||
cancelButton->installEventFilter(this);
|
||||
}
|
||||
|
||||
setTabOrder(m_instructionEdit, m_commandsComboBox);
|
||||
setTabOrder(m_commandsComboBox, okButton);
|
||||
setTabOrder(okButton, cancelButton);
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::createActionButtons()
|
||||
@@ -295,27 +320,12 @@ void QuickRefactorDialog::createActionButtons()
|
||||
|
||||
QString QuickRefactorDialog::instructions() const
|
||||
{
|
||||
QString result;
|
||||
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
if (!instruction.id.isEmpty()) {
|
||||
result = instruction.body;
|
||||
}
|
||||
|
||||
QString additionalText = m_textEdit->toPlainText().trimmed();
|
||||
if (!additionalText.isEmpty()) {
|
||||
if (!result.isEmpty()) {
|
||||
result += "\n\n";
|
||||
}
|
||||
result += additionalText;
|
||||
}
|
||||
|
||||
return result;
|
||||
return m_instructionEdit->toPlainText().trimmed();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::setInstructions(const QString &instructions)
|
||||
{
|
||||
m_textEdit->setPlainText(instructions);
|
||||
m_instructionEdit->setPlainText(instructions);
|
||||
}
|
||||
|
||||
QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
|
||||
@@ -323,17 +333,33 @@ QuickRefactorDialog::Action QuickRefactorDialog::selectedAction() const
|
||||
return m_selectedAction;
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
QDialog::keyPressEvent(event);
|
||||
}
|
||||
|
||||
bool QuickRefactorDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == m_textEdit && event->type() == QEvent::KeyPress) {
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
if (keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter) {
|
||||
if (keyEvent->modifiers() & Qt::ShiftModifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
accept();
|
||||
return true;
|
||||
if (watched == m_instructionEdit) {
|
||||
if (keyEvent->key() == Qt::Key_Tab) {
|
||||
m_commandsComboBox->setFocus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (watched == m_commandsComboBox || watched == m_commandsComboBox->lineEdit()) {
|
||||
if (keyEvent->key() == Qt::Key_Tab) {
|
||||
QPushButton *okButton = findChild<QPushButton *>();
|
||||
if (okButton && okButton->text() == "OK") {
|
||||
okButton->setFocus();
|
||||
} else {
|
||||
focusNextChild();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
@@ -343,8 +369,7 @@ void QuickRefactorDialog::useLastInstructions()
|
||||
{
|
||||
if (!m_lastInstructions.isEmpty()) {
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_commandsComboBox->clearEditText(); // Clear search text
|
||||
m_textEdit->setPlainText(m_lastInstructions);
|
||||
m_instructionEdit->setPlainText(m_lastInstructions);
|
||||
m_selectedAction = Action::RepeatLast;
|
||||
}
|
||||
accept();
|
||||
@@ -353,10 +378,10 @@ void QuickRefactorDialog::useLastInstructions()
|
||||
void QuickRefactorDialog::useImproveCodeTemplate()
|
||||
{
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_commandsComboBox->clearEditText(); // Clear search text
|
||||
m_textEdit->setPlainText(Tr::tr(
|
||||
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
|
||||
"Follow best practices for C++/Qt and fix any potential issues."));
|
||||
m_instructionEdit->setPlainText(
|
||||
Tr::tr(
|
||||
"Improve the selected code by enhancing readability, efficiency, and maintainability. "
|
||||
"Follow best practices for C++/Qt and fix any potential issues."));
|
||||
m_selectedAction = Action::ImproveCode;
|
||||
accept();
|
||||
}
|
||||
@@ -364,36 +389,29 @@ void QuickRefactorDialog::useImproveCodeTemplate()
|
||||
void QuickRefactorDialog::useAlternativeSolutionTemplate()
|
||||
{
|
||||
m_commandsComboBox->setCurrentIndex(0);
|
||||
m_commandsComboBox->clearEditText(); // Clear search text
|
||||
m_textEdit->setPlainText(
|
||||
Tr::tr("Suggest an alternative implementation approach for the selected code. "
|
||||
"Provide a different solution that might be cleaner, more efficient, "
|
||||
"or uses different Qt/C++ patterns or idioms."));
|
||||
m_instructionEdit->setPlainText(
|
||||
Tr::tr(
|
||||
"Suggest an alternative implementation approach for the selected code. "
|
||||
"Provide a different solution that might be cleaner, more efficient, "
|
||||
"or uses different Qt/C++ patterns or idioms."));
|
||||
m_selectedAction = Action::AlternativeSolution;
|
||||
accept();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::updateDialogSize()
|
||||
{
|
||||
QString text = m_textEdit->toPlainText();
|
||||
QString text = m_instructionEdit->toPlainText();
|
||||
|
||||
QFontMetrics fm(m_textEdit->font());
|
||||
QFontMetrics fm(m_instructionEdit->font());
|
||||
|
||||
QStringList lines = text.split('\n');
|
||||
int lineCount = lines.size();
|
||||
int lineCount = qMax(lines.size(), 3);
|
||||
|
||||
if (lineCount <= 1) {
|
||||
int singleLineHeight = fm.height() + 10;
|
||||
m_textEdit->setMinimumHeight(singleLineHeight);
|
||||
m_textEdit->setMaximumHeight(singleLineHeight);
|
||||
} else {
|
||||
m_textEdit->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||
m_instructionEdit->setMaximumHeight(QWIDGETSIZE_MAX);
|
||||
|
||||
int lineHeight = fm.height() + 2;
|
||||
|
||||
int textEditHeight = qMin(qMax(lineCount, 2) * lineHeight, 20 * lineHeight);
|
||||
m_textEdit->setMinimumHeight(textEditHeight);
|
||||
}
|
||||
int lineHeight = fm.height() + 2;
|
||||
int textEditHeight = qMin(qMax(lineCount, 3) * lineHeight, 15 * lineHeight);
|
||||
m_instructionEdit->setMinimumHeight(textEditHeight);
|
||||
|
||||
int maxWidth = 500;
|
||||
for (const QString &line : lines) {
|
||||
@@ -405,14 +423,7 @@ void QuickRefactorDialog::updateDialogSize()
|
||||
QRect screenGeometry = screen->availableGeometry();
|
||||
|
||||
int newWidth = qMin(maxWidth + 40, screenGeometry.width() * 3 / 4);
|
||||
|
||||
int newHeight;
|
||||
if (lineCount <= 1) {
|
||||
newHeight = 150;
|
||||
} else {
|
||||
newHeight = m_textEdit->minimumHeight() + 150;
|
||||
}
|
||||
newHeight = qMin(newHeight, screenGeometry.height() * 3 / 4);
|
||||
int newHeight = qMin(m_instructionEdit->minimumHeight() + 200, screenGeometry.height() * 3 / 4);
|
||||
|
||||
resize(newWidth, newHeight);
|
||||
}
|
||||
@@ -420,22 +431,16 @@ void QuickRefactorDialog::updateDialogSize()
|
||||
void QuickRefactorDialog::loadCustomCommands()
|
||||
{
|
||||
m_commandsComboBox->clear();
|
||||
m_commandsComboBox->addItem("", QString()); // Empty item for no selection
|
||||
m_commandsComboBox->addItem("", QString());
|
||||
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
const QVector<CustomInstruction> &instructions = manager.instructions();
|
||||
|
||||
QStringList instructionNames;
|
||||
int defaultInstructionIndex = -1;
|
||||
|
||||
for (int i = 0; i < instructions.size(); ++i) {
|
||||
const CustomInstruction &instruction = instructions[i];
|
||||
|
||||
for (const CustomInstruction &instruction : instructions) {
|
||||
m_commandsComboBox->addItem(instruction.name, instruction.id);
|
||||
instructionNames.append(instruction.name);
|
||||
|
||||
if (instruction.isDefault) {
|
||||
defaultInstructionIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_commandsComboBox->completer()) {
|
||||
@@ -443,10 +448,6 @@ void QuickRefactorDialog::loadCustomCommands()
|
||||
m_commandsComboBox->completer()->setModel(model);
|
||||
}
|
||||
|
||||
if (defaultInstructionIndex > 0) {
|
||||
m_commandsComboBox->setCurrentIndex(defaultInstructionIndex);
|
||||
}
|
||||
|
||||
bool hasInstructions = !instructions.isEmpty();
|
||||
m_editCommandButton->setEnabled(hasInstructions);
|
||||
m_deleteCommandButton->setEnabled(hasInstructions);
|
||||
@@ -461,13 +462,13 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
|
||||
|
||||
auto &manager = CustomInstructionsManager::instance();
|
||||
const QVector<CustomInstruction> &instructions = manager.instructions();
|
||||
|
||||
|
||||
for (const CustomInstruction &instruction : instructions) {
|
||||
if (instruction.name == currentText) {
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
int currentIndex = m_commandsComboBox->currentIndex();
|
||||
if (currentIndex > 0) {
|
||||
QString instructionId = m_commandsComboBox->itemData(currentIndex).toString();
|
||||
@@ -475,13 +476,20 @@ CustomInstruction QuickRefactorDialog::findCurrentInstruction() const
|
||||
return manager.getInstructionById(instructionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return CustomInstruction();
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onCommandSelected(int index)
|
||||
{
|
||||
Q_UNUSED(index);
|
||||
if (index <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
if (!instruction.id.isEmpty()) {
|
||||
m_instructionEdit->setPlainText(instruction.body);
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::onAddCustomCommand()
|
||||
@@ -493,10 +501,7 @@ void QuickRefactorDialog::onAddCustomCommand()
|
||||
|
||||
if (manager.saveInstruction(instruction)) {
|
||||
loadCustomCommands();
|
||||
|
||||
m_commandsComboBox->setCurrentText(instruction.name);
|
||||
|
||||
m_textEdit->clear();
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
@@ -509,10 +514,12 @@ void QuickRefactorDialog::onAddCustomCommand()
|
||||
void QuickRefactorDialog::onEditCustomCommand()
|
||||
{
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
|
||||
|
||||
if (instruction.id.isEmpty()) {
|
||||
QMessageBox::information(
|
||||
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to edit."));
|
||||
this,
|
||||
Tr::tr("No Instruction Selected"),
|
||||
Tr::tr("Please select an instruction to edit."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -524,7 +531,6 @@ void QuickRefactorDialog::onEditCustomCommand()
|
||||
if (manager.saveInstruction(updatedInstruction)) {
|
||||
loadCustomCommands();
|
||||
m_commandsComboBox->setCurrentText(updatedInstruction.name);
|
||||
m_textEdit->clear();
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
@@ -537,10 +543,12 @@ void QuickRefactorDialog::onEditCustomCommand()
|
||||
void QuickRefactorDialog::onDeleteCustomCommand()
|
||||
{
|
||||
CustomInstruction instruction = findCurrentInstruction();
|
||||
|
||||
|
||||
if (instruction.id.isEmpty()) {
|
||||
QMessageBox::information(
|
||||
this, Tr::tr("No Instruction Selected"), Tr::tr("Please select an instruction to delete."));
|
||||
this,
|
||||
Tr::tr("No Instruction Selected"),
|
||||
Tr::tr("Please select an instruction to delete."));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -569,19 +577,19 @@ void QuickRefactorDialog::onOpenInstructionsFolder()
|
||||
{
|
||||
QString path = QString("%1/qodeassist/quick_refactor/instructions")
|
||||
.arg(Core::ICore::userResourcePath().toFSPathString());
|
||||
|
||||
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
|
||||
|
||||
QUrl url = QUrl::fromLocalFile(dir.absolutePath());
|
||||
QDesktopServices::openUrl(url);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -594,8 +602,8 @@ void QuickRefactorDialog::loadAvailableConfigurations()
|
||||
auto &manager = Settings::ConfigurationManager::instance();
|
||||
manager.loadConfigurations(Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
QVector<Settings::AIConfiguration> configs
|
||||
= manager.configurations(Settings::ConfigurationType::QuickRefactor);
|
||||
QVector<Settings::AIConfiguration> configs = manager.configurations(
|
||||
Settings::ConfigurationType::QuickRefactor);
|
||||
|
||||
m_configComboBox->clear();
|
||||
m_configComboBox->addItem(Tr::tr("Current"), QString());
|
||||
@@ -640,4 +648,20 @@ void QuickRefactorDialog::onConfigurationChanged(int index)
|
||||
}
|
||||
}
|
||||
|
||||
void QuickRefactorDialog::validateAndAccept()
|
||||
{
|
||||
QString instruction = m_instructionEdit->toPlainText().trimmed();
|
||||
|
||||
if (instruction.isEmpty()) {
|
||||
QMessageBox::warning(
|
||||
this,
|
||||
Tr::tr("No Instruction"),
|
||||
Tr::tr("Please type an instruction or select a saved one."));
|
||||
m_instructionEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
accept();
|
||||
}
|
||||
|
||||
} // namespace QodeAssist
|
||||
|
||||
@@ -27,6 +27,8 @@ class QPlainTextEdit;
|
||||
class QToolButton;
|
||||
class QLabel;
|
||||
class QComboBox;
|
||||
class QLineEdit;
|
||||
class QFrame;
|
||||
|
||||
namespace QodeAssist {
|
||||
|
||||
@@ -49,6 +51,7 @@ public:
|
||||
QString selectedConfiguration() const;
|
||||
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void useLastInstructions();
|
||||
@@ -64,13 +67,14 @@ private slots:
|
||||
void loadCustomCommands();
|
||||
void loadAvailableConfigurations();
|
||||
void onConfigurationChanged(int index);
|
||||
void validateAndAccept();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
void createActionButtons();
|
||||
CustomInstruction findCurrentInstruction() const;
|
||||
|
||||
QPlainTextEdit *m_textEdit;
|
||||
QPlainTextEdit *m_instructionEdit;
|
||||
QToolButton *m_repeatButton;
|
||||
QToolButton *m_improveButton;
|
||||
QToolButton *m_alternativeButton;
|
||||
@@ -83,7 +87,6 @@ private:
|
||||
QToolButton *m_thinkingButton;
|
||||
QComboBox *m_commandsComboBox;
|
||||
QComboBox *m_configComboBox;
|
||||
QLabel *m_instructionsLabel;
|
||||
|
||||
Action m_selectedAction = Action::Custom;
|
||||
QString m_lastInstructions;
|
||||
|
||||
Reference in New Issue
Block a user