Compare commits

..

8 Commits

Author SHA1 Message Date
Petr Mironychev
142afa725f feat: Add using vector and chunks in Context Manager 2025-02-21 18:19:16 +01:00
Petr Mironychev
f36db033e6 refactor: Improvment rag storage 2025-02-09 10:53:35 +01:00
Petr Mironychev
5dfcf74128 feat: add chunking 2025-02-09 10:10:06 +01:00
Petr Mironychev
02101665ca feat: Add version to vector db 2025-02-09 01:00:30 +01:00
Petr Mironychev
77a03d42ed feat: add enhancedSearch 2025-02-09 00:04:45 +01:00
Petr Mironychev
09c38c8b0e fix: Rebase on main 2025-02-09 00:04:45 +01:00
Petr Mironychev
7b73d7af7b feat: Add similarity search 2025-02-09 00:04:44 +01:00
Petr Mironychev
5a426b4d9f feat: RAG init 2025-02-09 00:04:44 +01:00
300 changed files with 5444 additions and 19429 deletions

2
.github/FUNDING.yml vendored
View File

@@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: ['https://www.paypal.com/paypalme/palm1r', 'https://github.com/Palm1r/QodeAssist#support-the-development-of-qodeassist']
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -6,77 +6,22 @@
"llm",
"ai"
],
"compatibility": "Qt 6.8.3",
"compatibility": "Qt 6.8.1",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.5.11",
"version": "0.4.0",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": false,
"released_at": "2024-01-24T15:00:00Z"
},
{
"version": "0.5.2",
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": false,
"released_at": "2025-03-14T11:00:00Z"
},
{
"version": "0.5.4",
"is_latest": false,
"released_at": "2025-03-17T03:00:00Z"
},
{
"version": "0.5.5",
"is_latest": false,
"released_at": "2025-03-20T19:00:00Z"
},
{
"version": "0.5.6",
"is_latest": false,
"released_at": "2025-04-04T19:00:00Z"
},
{
"version": "0.5.7",
"is_latest": false,
"released_at": "2025-04-14T01:00:00Z"
},
{
"version": "0.5.8",
"is_latest": false,
"released_at": "2025-04-17T10:00:00Z"
},
{
"version": "0.5.9",
"is_latest": false,
"released_at": "2025-04-21T10:00:00Z"
},
{
"version": "0.5.10",
"is_latest": false,
"released_at": "2025-04-24T10:00:00Z"
},
{
"version": "0.5.11",
"is_latest": false,
"released_at": "2025-04-24T21:00:00Z"
},
{
"version": "0.5.12",
"is_latest": true,
"released_at": "2025-05-01T17:00:00Z"
"released_at": "2024-01-24T15:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",

View File

@@ -12,13 +12,16 @@ on:
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.1
QT_CREATOR_VERSION: 15.0.1
QT_CREATOR_VERSION_INTERNAL: 15.0.1
MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1"
jobs:
build:
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
name: ${{ matrix.config.name }}
runs-on: ${{ matrix.config.os }}
outputs:
tag: ${{ steps.git.outputs.tag }}
@@ -33,8 +36,8 @@ jobs:
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
}
- {
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
os: ubuntu-22.04,
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-latest,
platform: linux_x64,
cc: "gcc", cxx: "g++"
}
@@ -44,18 +47,12 @@ jobs:
platform: mac_x64,
cc: "clang", cxx: "clang++"
}
qt_config:
- {
qt_version: "6.9.2",
qt_creator_version: "17.0.2"
}
- {
qt_version: "6.8.3",
qt_creator_version: "16.0.2"
}
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Checkout submodules
id: git
@@ -64,21 +61,16 @@ jobs:
if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
else()
execute_process(
COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE short_sha
OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
endif()
- name: Download Ninja and CMake
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
uses: lukka/get-cmake@latest
with:
cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }}
- name: Install dependencies
- name: Install system libs
shell: cmake -P {0}
run: |
if ("${{ runner.os }}" STREQUAL "Linux")
@@ -86,13 +78,7 @@ jobs:
COMMAND sudo apt update
)
execute_process(
COMMAND sudo apt install
# build dependencies
libgl1-mesa-dev libgtest-dev libgmock-dev
# runtime dependencies for tests (Qt is downloaded outside package manager,
# thus minimal dependencies must be installed explicitly)
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
COMMAND sudo apt install libgl1-mesa-dev
RESULT_VARIABLE result
)
if (NOT result EQUAL 0)
@@ -104,7 +90,7 @@ jobs:
id: qt
shell: cmake -P {0}
run: |
set(qt_version "${{ matrix.qt_config.qt_version }}")
set(qt_version "$ENV{QT_VERSION}")
string(REPLACE "." "" qt_version_dotless "${qt_version}")
if ("${{ runner.os }}" STREQUAL "Windows")
@@ -125,11 +111,7 @@ jobs:
set(url_os "mac_x64")
set(qt_package_arch_suffix "clang_64")
set(qt_dir_prefix "${qt_version}/macos")
if (qt_version VERSION_LESS "6.9.1")
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
else()
set(qt_package_suffix "-MacOS-MacOS_15-Clang-MacOS-MacOS_15-X86_64-ARM64")
endif()
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
@@ -152,7 +134,7 @@ jobs:
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
endfunction()
foreach(package qtbase qtdeclarative qttools)
foreach(package qtbase qtdeclarative)
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
@@ -186,11 +168,10 @@ jobs:
endif()
- name: Download Qt Creator
uses: qt-creator/install-dev-package@4046eda2efa77c0fe61d4cde7e622c050a4d65af
uses: qt-creator/install-dev-package@v1.2
with:
version: ${{ matrix.qt_config.qt_creator_version }}
version: ${{ env.QT_CREATOR_VERSION }}
unzip-to: 'qtcreator'
platform: ${{ matrix.config.platform }}
- name: Extract Qt Creator
id: qt_creator
@@ -236,7 +217,7 @@ jobs:
COMMAND python
-u
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}"
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}"
--name "$ENV{PLUGIN_NAME}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
--src .
--build build
--qt-path "${{ steps.qt.outputs.qt_dir }}"
@@ -252,24 +233,67 @@ jobs:
endif()
- name: Upload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@v4
with:
path: ./${{ env.PLUGIN_NAME }}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
# The json is the same for all platforms, but we need to save one
- name: Upload plugin json
if: matrix.config.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Download the JSON file
uses: actions/download-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./${{ env.PLUGIN_NAME }}-origin
- name: Store Release upload_url
run: |
xvfb-run ./build/build/test/QodeAssistTest
RELEASE_HTML_URL=$(echo "${{github.event.repository.html_url}}/releases/download/v${{ needs.build.outputs.tag }}")
echo "RELEASE_HTML_URL=${RELEASE_HTML_URL}" >> $GITHUB_ENV
- name: Run the Node.js script to update JSON
env:
QT_TOKEN: ${{ secrets.TOKEN }}
API_URL: ${{ secrets.API_URL }}
run: |
node .github/scripts/registerPlugin.js ${{ env.RELEASE_HTML_URL }} ${{ env.PLUGIN_NAME }} ${{ env.QT_CREATOR_VERSION }} ${{ env.QT_CREATOR_VERSION_INTERNAL }} ${{ env.QT_TOKEN }} ${{ env.API_URL }}
- name: Delete previous json artifacts
uses: geekyeggo/delete-artifact@v5
with:
name: ${{ env.PLUGIN_NAME }}*-json
- name: Upload the modified JSON file as an artifact
uses: actions/upload-artifact@v4
with:
name: plugin-json
path: .github/scripts/${{ env.PLUGIN_NAME }}.json
release:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-22.04
needs: [build]
runs-on: ubuntu-latest
needs: [build, update_json]
steps:
- name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
uses: actions/download-artifact@v4
with:
path: release-with-dirs
@@ -280,7 +304,7 @@ jobs:
- name: Create Release
id: create_release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
uses: softprops/action-gh-release@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

3
.gitignore vendored
View File

@@ -73,5 +73,4 @@ CMakeLists.txt.user*
*.dll
*.exe
/build
/.qodeassist
/build

0
.gitmodules vendored
View File

View File

@@ -8,41 +8,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
add_subdirectory(llmcore)
add_subdirectory(settings)
add_subdirectory(logger)
add_subdirectory(UIControls)
add_subdirectory(ChatView)
add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS
@@ -69,34 +43,26 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/DeepSeekCoderFim.hpp
templates/CustomFimTemplate.hpp
templates/Qwen.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
@@ -106,46 +72,4 @@ add_qtc_plugin(QodeAssist
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ReadProjectFileByNameTool.hpp tools/ReadProjectFileByNameTool.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
tools/SearchInProjectTool.hpp tools/SearchInProjectTool.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()
#TODO change to TS_OUTPUT_DIRECTORY after removing Qt6.8
qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete
)

View File

@@ -6,19 +6,17 @@ qt_policy(SET QTP0004 NEW)
qt_add_qml_module(QodeAssistChatView
URI ChatView
VERSION 1.0
DEPENDENCIES
QtQuick
DEPENDENCIES QtQuick
QML_FILES
qml/RootItem.qml
qml/ChatItem.qml
qml/Badge.qml
qml/dialog/CodeBlock.qml
qml/dialog/TextBlock.qml
qml/controls/QoAButton.qml
qml/parts/TopBar.qml
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
qml/parts/ErrorToast.qml
qml/ToolStatusItem.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg
@@ -26,14 +24,6 @@ qt_add_qml_module(QodeAssistChatView
icons/close-light.svg
icons/link-file-light.svg
icons/link-file-dark.svg
icons/load-chat-dark.svg
icons/save-chat-dark.svg
icons/clean-icon-dark.svg
icons/file-in-system.svg
icons/window-lock.svg
icons/window-unlock.svg
icons/chat-icon.svg
icons/chat-pause-icon.svg
SOURCES
ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp
@@ -42,8 +32,6 @@ qt_add_qml_module(QodeAssistChatView
MessagePart.hpp
ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp
ChatView.hpp ChatView.cpp
ChatData.hpp
)
target_link_libraries(QodeAssistChatView
@@ -57,7 +45,6 @@ target_link_libraries(QodeAssistChatView
LLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
)
target_include_directories(QodeAssistChatView

View File

@@ -1,32 +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 <QObject>
#include <QtQmlIntegration>
namespace QodeAssist::Chat {
Q_NAMESPACE
QML_NAMED_ELEMENT(MessagePartType)
enum class MessagePartType { Code, Text };
Q_ENUM_NS(MessagePartType)
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -18,12 +18,11 @@
*/
#include "ChatModel.hpp"
#include <utils/aspects.h>
#include <QtCore/qjsonobject.h>
#include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp"
#include "Logger.hpp"
namespace QodeAssist::Chat {
@@ -32,11 +31,10 @@ ChatModel::ChatModel(QObject *parent)
{
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
}
int ChatModel::rowCount(const QModelIndex &parent) const
@@ -92,8 +90,7 @@ void ChatModel::addMessage(
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
&& m_messages.last().role == role) {
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
Message &lastMessage = m_messages.last();
lastMessage.content = content;
lastMessage.attachments = attachments;
@@ -126,39 +123,23 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
int lastIndex = 0;
auto blockMatches = codeBlockRegex.globalMatch(content);
bool foundCodeBlock = blockMatches.hasNext();
while (blockMatches.hasNext()) {
auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) {
QString textBetween
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) {
parts.append({MessagePartType::Text, textBetween, ""});
parts.append({MessagePart::Text, textBetween, ""});
}
}
parts.append({MessagePartType::Code, match.captured(2).trimmed(), match.captured(1)});
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
lastIndex = match.capturedEnd();
}
if (lastIndex < content.length()) {
QString remainingText = content.mid(lastIndex).trimmed();
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
if (unclosedMatch.hasMatch()) {
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
if (!beforeCodeBlock.isEmpty()) {
parts.append({MessagePartType::Text, beforeCodeBlock, ""});
}
parts.append(
{MessagePartType::Code,
unclosedMatch.captured(2).trimmed(),
unclosedMatch.captured(1)});
} else if (!remainingText.isEmpty()) {
parts.append({MessagePartType::Text, remainingText, ""});
if (!remainingText.isEmpty()) {
parts.append({MessagePart::Text, remainingText, ""});
}
}
@@ -214,69 +195,4 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : "";
}
void ChatModel::resetModelTo(int index)
{
if (index < 0 || index >= m_messages.size())
return;
if (index < m_messages.size()) {
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
endRemoveRows();
}
}
void ChatModel::addToolExecutionStatus(
const QString &requestId, const QString &toolId, const QString &toolName)
{
QString content = toolName;
LOG_MESSAGE(QString("Adding tool execution status: requestId=%1, toolId=%2, toolName=%3")
.arg(requestId, toolId, toolName));
if (!m_messages.isEmpty() && !toolId.isEmpty() && m_messages.last().id == toolId
&& m_messages.last().role == ChatRole::Tool) {
Message &lastMessage = m_messages.last();
lastMessage.content = content;
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{ChatRole::Tool, content, toolId};
m_messages.append(newMessage);
endInsertRows();
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
.arg(m_messages.size() - 1)
.arg(toolId));
}
}
void ChatModel::updateToolResult(
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
{
if (m_messages.isEmpty() || toolId.isEmpty()) {
LOG_MESSAGE(QString("Cannot update tool result: messages empty=%1, toolId empty=%2")
.arg(m_messages.isEmpty())
.arg(toolId.isEmpty()));
return;
}
LOG_MESSAGE(
QString("Updating tool result: requestId=%1, toolId=%2, toolName=%3, result length=%4")
.arg(requestId, toolId, toolName)
.arg(result.length()));
for (int i = m_messages.size() - 1; i >= 0; --i) {
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
m_messages[i].content = toolName + "\n" + result;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
return;
}
}
LOG_MESSAGE(QString("WARNING: Tool message with requestId=%1 toolId=%2 not found!")
.arg(requestId, toolId));
}
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -37,11 +37,10 @@ class ChatModel : public QAbstractListModel
QML_ELEMENT
public:
enum ChatRole { System, User, Assistant, Tool };
enum ChatRole { System, User, Assistant };
Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
Q_ENUM(Roles)
struct Message
{
@@ -74,15 +73,6 @@ public:
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
void addToolExecutionStatus(
const QString &requestId, const QString &toolId, const QString &toolName);
void updateToolResult(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
signals:
void tokensThresholdChanged();
void modelReseted();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -29,6 +29,7 @@
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <projectexplorer/projecttree.h>
#include <utils/theme/theme.h>
#include <utils/utilsicons.h>
@@ -38,6 +39,8 @@
#include "Logger.hpp"
#include "ProjectSettings.hpp"
#include "context/ContextManager.hpp"
#include "context/FileChunker.hpp"
#include "context/RAGManager.hpp"
#include "context/TokenUtils.hpp"
namespace QodeAssist::Chat {
@@ -45,21 +48,21 @@ namespace QodeAssist::Chat {
ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent)
, m_chatModel(new ChatModel(this))
, m_promptProvider(LLMCore::PromptTemplateManager::instance())
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_isRequestInProgress(false)
, m_clientInterface(new ClientInterface(m_chatModel, this))
{
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect(
&Settings::chatAssistantSettings().linkOpenFiles,
&Utils::BaseAspect::changed,
this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); });
connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
this,
[this](){
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
});
auto &settings = Settings::generalSettings();
connect(
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged);
connect(&settings.caModel,
&Utils::BaseAspect::changed,
this,
&ChatRootView::currentTemplateChanged);
connect(
m_clientInterface,
@@ -67,10 +70,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this,
&ChatRootView::autosave);
connect(m_clientInterface, &ClientInterface::messageReceivedCompletely, this, [this]() {
this->setRequestProgressStatus(false);
});
connect(
m_clientInterface,
&ClientInterface::messageReceivedCompletely,
@@ -80,16 +79,10 @@ ChatRootView::ChatRootView(QQuickItem *parent)
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().useSystemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
this, &ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance();
@@ -107,36 +100,6 @@ ChatRootView::ChatRootView(QQuickItem *parent)
}
}
});
connect(
&Settings::chatAssistantSettings().textFontFamily,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFamilyChanged);
connect(
&Settings::chatAssistantSettings().codeFontFamily,
&Utils::BaseAspect::changed,
this,
&ChatRootView::codeFamilyChanged);
connect(
&Settings::chatAssistantSettings().textFontSize,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFontSizeChanged);
connect(
&Settings::chatAssistantSettings().codeFontSize,
&Utils::BaseAspect::changed,
this,
&ChatRootView::codeFontSizeChanged);
connect(
&Settings::chatAssistantSettings().textFormat,
&Utils::BaseAspect::changed,
this,
&ChatRootView::textFormatChanged);
connect(m_clientInterface, &ClientInterface::errorOccurred, this, [this](const QString &error) {
this->setRequestProgressStatus(false);
m_lastErrorMessage = error;
emit lastErrorMessageChanged();
});
updateInputTokensCount();
}
@@ -166,7 +129,6 @@ void ChatRootView::sendMessage(const QString &message)
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
clearAttachmentFiles();
setRequestProgressStatus(true);
}
void ChatRootView::copyToClipboard(const QString &text)
@@ -177,7 +139,6 @@ void ChatRootView::copyToClipboard(const QString &text)
void ChatRootView::cancelRequest()
{
m_clientInterface->cancelRequest();
setRequestProgressStatus(false);
}
void ChatRootView::clearAttachmentFiles()
@@ -202,10 +163,9 @@ QString ChatRootView::getChatsHistoryDir() const
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
path = projectSettings.chatHistoryPath().toString();
} else {
path = QString("%1/qodeassist/chat_history")
.arg(Core::ICore::userResourcePath().toFSPathString());
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
}
QDir dir(path);
@@ -389,7 +349,7 @@ void ChatRootView::showAttachFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString());
dialog.setDirectory(project->projectDirectory().toString());
}
if (dialog.exec() == QDialog::Accepted) {
@@ -423,7 +383,7 @@ void ChatRootView::showLinkFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString());
dialog.setDirectory(project->projectDirectory().toString());
}
if (dialog.exec() == QDialog::Accepted) {
@@ -476,10 +436,9 @@ void ChatRootView::openChatHistoryFolder()
QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString();
path = projectSettings.chatHistoryPath().toString();
} else {
path = QString("%1/qodeassist/chat_history")
.arg(Core::ICore::userResourcePath().toFSPathString());
path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
}
QDir dir(path);
@@ -491,27 +450,90 @@ void ChatRootView::openChatHistoryFolder()
QDesktopServices::openUrl(url);
}
// ChatRootView.cpp
void ChatRootView::testRAG(const QString &message)
{
auto project = ProjectExplorer::ProjectTree::currentProject();
if (!project) {
qDebug() << "No active project found";
return;
}
const QString TEST_QUERY = message;
qDebug() << "Starting RAG test with query:";
qDebug() << TEST_QUERY;
qDebug() << "\nFirst, processing project files...";
auto files = Context::ContextManager::instance().getProjectSourceFiles(project);
// Было: auto future = Context::RAGManager::instance().processFiles(project, files);
// Стало:
auto future = Context::RAGManager::instance().processProjectFiles(project, files);
connect(
&Context::RAGManager::instance(),
&Context::RAGManager::vectorizationProgress,
this,
[](int processed, int total) {
qDebug() << QString("Vectorization progress: %1 of %2 files").arg(processed).arg(total);
});
connect(
&Context::RAGManager::instance(),
&Context::RAGManager::vectorizationFinished,
this,
[this, project, TEST_QUERY]() {
qDebug() << "\nVectorization completed. Starting similarity search...\n";
// Было: Context::RAGManager::instance().searchSimilarDocuments(TEST_QUERY, project, 5);
// Стало:
auto future = Context::RAGManager::instance().findRelevantChunks(TEST_QUERY, project, 5);
future.then([](const QList<Context::RAGManager::ChunkSearchResult> &results) {
qDebug() << "Found" << results.size() << "relevant chunks:";
for (const auto &result : results) {
qDebug() << "File:" << result.filePath;
qDebug() << "Lines:" << result.startLine << "-" << result.endLine;
qDebug() << "Score:" << result.combinedScore;
qDebug() << "Content:" << result.content;
qDebug() << "---";
}
});
});
}
void ChatRootView::testChunking()
{
auto project = ProjectExplorer::ProjectTree::currentProject();
if (!project) {
qDebug() << "No active project found";
return;
}
Context::FileChunker::ChunkingConfig config;
Context::ContextManager::instance().testProjectChunks(project, config);
}
void ChatRootView::updateInputTokensCount()
{
int inputTokens = m_messageTokensCount;
auto &settings = Settings::chatAssistantSettings();
auto& settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
}
if (!m_attachmentFiles.isEmpty()) {
auto attachFiles = m_clientInterface->contextManager()->getContentFiles(m_attachmentFiles);
auto attachFiles = Context::ContextManager::instance().getContentFiles(m_attachmentFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
}
if (!m_linkedFiles.isEmpty()) {
auto linkFiles = m_clientInterface->contextManager()->getContentFiles(m_linkedFiles);
auto linkFiles = Context::ContextManager::instance().getContentFiles(m_linkedFiles);
inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
}
const auto &history = m_chatModel->getChatHistory();
for (const auto &message : history) {
const auto& history = m_chatModel->getChatHistory();
for (const auto& message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role
}
@@ -533,7 +555,7 @@ bool ChatRootView::isSyncOpenFiles() const
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString();
QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged();
}
@@ -546,8 +568,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{
if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString();
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) {
QString filePath = document->filePath().toString();
if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath);
emit linkedFilesChanged();
}
@@ -574,62 +596,4 @@ void ChatRootView::setRecentFilePath(const QString &filePath)
}
}
bool ChatRootView::shouldIgnoreFileForAttach(const Utils::FilePath &filePath)
{
auto project = ProjectExplorer::ProjectManager::projectForFile(filePath);
if (project
&& m_clientInterface->contextManager()
->ignoreManager()
->shouldIgnore(filePath.toFSPathString(), project)) {
LOG_MESSAGE(QString("Ignoring file for attachment due to .qodeassistignore: %1")
.arg(filePath.toFSPathString()));
return true;
}
return false;
}
QString ChatRootView::textFontFamily() const
{
return Settings::chatAssistantSettings().textFontFamily.stringValue();
}
QString ChatRootView::codeFontFamily() const
{
return Settings::chatAssistantSettings().codeFontFamily.stringValue();
}
int ChatRootView::codeFontSize() const
{
return Settings::chatAssistantSettings().codeFontSize();
}
int ChatRootView::textFontSize() const
{
return Settings::chatAssistantSettings().textFontSize();
}
int ChatRootView::textFormat() const
{
return Settings::chatAssistantSettings().textFormat();
}
bool ChatRootView::isRequestInProgress() const
{
return m_isRequestInProgress;
}
void ChatRootView::setRequestProgressStatus(bool state)
{
if (m_isRequestInProgress == state)
return;
m_isRequestInProgress = state;
emit isRequestInProgressChanged();
}
QString ChatRootView::lastErrorMessage() const
{
return m_lastErrorMessage;
}
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -23,7 +23,6 @@
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat {
@@ -38,14 +37,6 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
Q_PROPERTY(
bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
QML_ELEMENT
@@ -74,6 +65,8 @@ public:
Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void testRAG(const QString &message);
Q_INVOKABLE void testChunking();
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
@@ -86,19 +79,6 @@ public:
QString chatFileName() const;
void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
QString textFontFamily() const;
QString codeFontFamily() const;
int codeFontSize() const;
int textFontSize() const;
int textFormat() const;
bool isRequestInProgress() const;
void setRequestProgressStatus(bool state);
QString lastErrorMessage() const;
public slots:
void sendMessage(const QString &message);
@@ -115,22 +95,12 @@ signals:
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
void textFamilyChanged();
void codeFamilyChanged();
void codeFontSizeChanged();
void textFontSizeChanged();
void textFormatChanged();
void chatRequestStarted();
void isRequestInProgressChanged();
void lastErrorMessageChanged();
private:
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface;
QString m_currentTemplate;
QString m_recentFilePath;
@@ -140,8 +110,6 @@ private:
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
};
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -29,40 +29,4 @@ void ChatUtils::copyToClipboard(const QString &text)
QGuiApplication::clipboard()->setText(text);
}
QString ChatUtils::getSafeMarkdownText(const QString &text) const
{
if (text.isEmpty()) {
return text;
}
bool needsSanitization = false;
for (const QChar &ch : text) {
if (ch.isNull() || (!ch.isPrint() && ch != '\n' && ch != '\t' && ch != '\r' && ch != ' ')) {
needsSanitization = true;
break;
}
}
if (!needsSanitization) {
return text;
}
QString safeText;
safeText.reserve(text.size());
for (QChar ch : text) {
if (ch.isNull()) {
safeText.append(' ');
} else if (ch == '\n' || ch == '\t' || ch == '\r' || ch == ' ') {
safeText.append(ch);
} else if (ch.isPrint()) {
safeText.append(ch);
} else {
safeText.append(QChar(0xFFFD));
}
}
return safeText;
}
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -34,7 +34,6 @@ public:
: QObject(parent) {};
Q_INVOKABLE void copyToClipboard(const QString &text);
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
};
} // namespace QodeAssist::Chat

View File

@@ -1,106 +0,0 @@
/*
* Copyright (C) 2024-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 "ChatView.hpp"
#include <QQmlContext>
#include <QQmlEngine>
#include <QSettings>
#include <QVariantMap>
#include <coreplugin/actionmanager/actionmanager.h>
#include <logger/Logger.hpp>
namespace {
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
| Qt::WindowCloseButtonHint;
}
namespace QodeAssist::Chat {
ChatView::ChatView()
: m_isPin(false)
{
setTitle("QodeAssist Chat");
engine()->rootContext()->setContextProperty("_chatview", this);
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
setResizeMode(QQuickView::SizeRootObjectToView);
setMinimumSize({400, 300});
setFlags(baseFlags);
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
m_closeShortcut = new QShortcut(action->keySequence(), this);
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
if (m_closeShortcut) {
m_closeShortcut->setKey(action->keySequence());
}
});
}
restoreSettings();
}
void ChatView::closeEvent(QCloseEvent *event)
{
saveSettings();
event->accept();
}
void ChatView::saveSettings()
{
QSettings settings;
settings.setValue("QodeAssist/ChatView/geometry", geometry());
settings.setValue("QodeAssist/ChatView/pinned", m_isPin);
}
void ChatView::restoreSettings()
{
QSettings settings;
const QRect savedGeometry
= settings.value("QodeAssist/ChatView/geometry", QRect(100, 100, 800, 600)).toRect();
setGeometry(savedGeometry);
const bool pinned = settings.value("QodeAssist/ChatView/pinned", false).toBool();
setIsPin(pinned);
}
bool ChatView::isPin() const
{
return m_isPin;
}
void ChatView::setIsPin(bool newIsPin)
{
if (m_isPin == newIsPin)
return;
m_isPin = newIsPin;
if (m_isPin) {
setFlags(baseFlags | Qt::WindowStaysOnTopHint);
} else {
setFlags(baseFlags);
}
emit isPinChanged();
}
} // namespace QodeAssist::Chat

View File

@@ -1,51 +0,0 @@
/*
* Copyright (C) 2024-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 <QQuickView>
#include <QShortcut>
namespace QodeAssist::Chat {
class ChatView : public QQuickView
{
Q_OBJECT
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
public:
ChatView();
bool isPin() const;
void setIsPin(bool newIsPin);
signals:
void isPinChanged();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void saveSettings();
void restoreSettings();
bool m_isPin;
QShortcut *m_closeShortcut;
};
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
{
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
}
} // namespace QodeAssist::Chat
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -38,4 +38,4 @@ signals:
void clearPressed();
};
} // namespace QodeAssist::Chat
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -19,38 +19,49 @@
#include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include "ChatAssistantSettings.hpp"
#include "ContextManager.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "RequestConfig.hpp"
#include <RulesLoader.hpp>
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
: QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{}
{
connect(m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
handleLLMResponse(completion, request, isComplete);
});
connect(m_requestHandler,
&LLMCore::RequestHandler::requestFinished,
this,
[this](const QString &, bool success, const QString &errorString) {
if (!success) {
emit errorOccurred(errorString);
}
});
}
ClientInterface::~ClientInterface() = default;
@@ -58,9 +69,8 @@ void ClientInterface::sendMessage(
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
{
cancelRequest();
m_accumulatedResponses.clear();
auto attachFiles = m_contextManager->getContentFiles(attachments);
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
@@ -74,7 +84,8 @@ void ClientInterface::sendMessage(
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -82,98 +93,52 @@ void ClientInterface::sendMessage(
}
LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
QString systemPrompt;
if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
}
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
QVector<LLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
}
context.history = messages;
QJsonObject providerRequest;
providerRequest["model"] = Settings::generalSettings().caModel();
providerRequest["stream"] = chatAssistantSettings.stream();
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
}
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest = providerRequest;
config.multiLineCompletion = false;
config.apiKey = provider->apiKey();
config.provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QJsonObject request;
request["id"] = QUuid::createUuid().toString();
QString requestId = QUuid::createUuid().toString();
QJsonObject request{{"id", requestId}};
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for chat request:");
LOG_MESSAGES(errors);
return;
}
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::partialResponseReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionStarted,
m_chatModel,
&ChatModel::addToolExecutionStatus,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionCompleted,
m_chatModel,
&ChatModel::updateToolResult,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::continuationStarted,
this,
&ClientInterface::handleCleanAccumulatedData,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
m_requestHandler->sendLLMRequest(config, request);
}
void ClientInterface::clearMessages()
@@ -184,21 +149,13 @@ void ClientInterface::clearMessages()
void ClientInterface::cancelRequest()
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
m_accumulatedResponses.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
auto id = m_chatModel->lastMessageId();
m_requestHandler->cancelRequest(id);
}
void ClientInterface::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
void ClientInterface::handleLLMResponse(const QString &response,
const QJsonObject &request,
bool isComplete)
{
const auto message = response.trimmed();
@@ -229,81 +186,30 @@ QString ClientInterface::getCurrentFileContext() const
}
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
.arg(textDocument->mimeType(), textDocument->filePath().toString());
QString content = textDocument->document()->toPlainText();
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
}
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
.arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest, false);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
handleLLMResponse(finalText, ctx.originalRequest, true);
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
}
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error);
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
}
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
{
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -24,9 +24,7 @@
#include <QVector>
#include "ChatModel.hpp"
#include "Provider.hpp"
#include "llmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
#include "RequestHandler.hpp"
namespace QodeAssist::Chat {
@@ -35,8 +33,7 @@ class ClientInterface : public QObject
Q_OBJECT
public:
explicit ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
~ClientInterface();
void sendMessage(
@@ -46,36 +43,19 @@ public:
void clearMessages();
void cancelRequest();
Context::ContextManager *contextManager() const;
signals:
void errorOccurred(const QString &error);
void messageReceivedCompletely();
private slots:
void handlePartialResponse(const QString &requestId, const QString &partialText);
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
void handleCleanAccumulatedData(const QString &requestId);
private:
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const;
const QString &basePrompt,
const QList<QString> &linkedFiles) const;
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::RequestHandler *m_requestHandler;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
};
} // namespace QodeAssist::Chat

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -19,24 +19,33 @@
#pragma once
#include <QObject>
#include <QtQmlIntegration>
#include "ChatData.hpp"
#include <qobject.h>
#include <qqmlintegration.h>
namespace QodeAssist::Chat {
Q_NAMESPACE
class MessagePart
{
Q_GADGET
Q_PROPERTY(MessagePartType type MEMBER type CONSTANT FINAL)
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
Q_PROPERTY(QString text MEMBER text CONSTANT FINAL)
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
QML_VALUE_TYPE(messagePart)
public:
MessagePartType type;
enum PartType { Code, Text };
Q_ENUM(PartType)
PartType type;
QString text;
QString language;
};
class MessagePartType : public MessagePart
{
Q_GADGET
};
QML_NAMED_ELEMENT(MessagePart)
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
} // namespace QodeAssist::Chat

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5_6)">
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_5_6">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 523 B

View File

@@ -1,10 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_5_17)">
<path d="M21.6 0H2.4C1.08 0 0 1.08 0 2.4V16.8C0 18.12 1.08 19.2 2.4 19.2H7.2V22.8C7.2 23.46 7.74 24 8.4 24H9C9.3 24 9.6 23.88 9.84 23.652L14.28 19.2H21.6C22.92 19.2 24 18.12 24 16.8V2.4C24 1.08 22.92 0 21.6 0ZM21.6 16.8H13.44L8.76 21.48L8.4 21.6V16.8H2.4V2.4H21.6V16.8ZM8.4 6H15.6V13.2H8.4V6Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_5_17">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 548 B

View File

@@ -1,8 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.75 15H3.25C2.00736 15 1 16.0074 1 17.25V39.75C1 40.9926 2.00736 42 3.25 42H16.75C17.9926 42 19 40.9926 19 39.75V17.25C19 16.0074 17.9926 15 16.75 15Z" stroke="black" stroke-width="2"/>
<path d="M1.04316 11.015L18.9554 8.90787" stroke="black" stroke-width="2" stroke-linecap="round"/>
<path d="M7.19462 10.363L7.02032 8.59516C6.92446 7.62284 8.18688 6.64116 9.8257 6.41365C11.4645 6.18615 12.8838 6.79555 12.9797 7.76787L13.154 9.53573" stroke="black" stroke-width="2"/>
<path d="M6 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M10 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M14 24V34" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 822 B

View File

@@ -1,12 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_59_114)">
<path d="M2 8H12L16 4H40C42 4 44 6 44 8V36C44 38 42 40 40 40H6C4 40 2 38 2 36V8Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="3"/>
<path d="M25 37C32.732 37 39 30.732 39 23C39 15.268 32.732 9 25 9C17.268 9 11 15.268 11 23C11 30.732 17.268 37 25 37Z" stroke="black" stroke-width="4"/>
<path d="M33 35L42 44" stroke="black" stroke-width="4" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_59_114">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 624 B

View File

@@ -1,5 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 8H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
<path d="M10 16V36" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
<path d="M5 21L10 16L15 21" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@@ -1,5 +0,0 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10 8V28" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
<path d="M5 23L10 28L15 23" stroke="black" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 36H15" stroke="black" stroke-width="2.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@@ -1,5 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.3" stroke="black" stroke-width="4"/>
<path d="M14 18V10C14 5.6 17.6 2 22 2C26.4 2 30 5.6 30 10V18" stroke="black" stroke-width="4"/>
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 552 B

View File

@@ -1,5 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31 18H13C11.3431 18 10 19.3431 10 21V37C10 38.6569 11.3431 40 13 40H31C32.6569 40 34 38.6569 34 37V21C34 19.3431 32.6569 18 31 18Z" fill="black" fill-opacity="0.1" stroke="black" stroke-width="4"/>
<path d="M14 17V9.5C14 5.375 17.15 2 21 2C24.85 2 27.5 2.875 27.5 7" stroke="black" stroke-width="4"/>
<path d="M22 32C23.6569 32 25 30.6569 25 29C25 27.3431 23.6569 26 22 26C20.3431 26 19 27.3431 19 29C19 30.6569 20.3431 32 22 32Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 559 B

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -23,7 +23,6 @@ Rectangle {
id: root
property alias text: badgeText.text
property alias hovered: mouse.hovered
implicitWidth: badgeText.implicitWidth + root.radius
implicitHeight: badgeText.implicitHeight + 6
@@ -38,10 +37,4 @@ Rectangle {
anchors.centerIn: parent
color: palette.buttonText
}
HoverHandler {
id: mouse
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -20,8 +20,6 @@
import QtQuick
import ChatView
import QtQuick.Layouts
import UIControls
import "./dialog"
Rectangle {
@@ -29,42 +27,14 @@ Rectangle {
property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property string textFontFamily: Qt.application.font.family
property string codeFontFamily: {
switch (Qt.platform.os) {
case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
}
}
property int textFontSize: Qt.application.font.pointSize
property int codeFontSize: Qt.application.font.pointSize
property int textFormat: 0
property bool isUserMessage: false
property int messageIndex: -1
signal resetChatToMessage(int index)
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout {
id: msgColumn
x: 5
width: parent.width - x
width: parent.width
anchors.verticalCenter: parent.verticalCenter
spacing: 5
@@ -86,8 +56,8 @@ Rectangle {
}
switch(modelData.type) {
case MessagePartType.Text: return textComponent;
case MessagePartType.Code: return codeBlockComponent;
case MessagePart.Text: return textComponent;
case MessagePart.Code: return codeBlockComponent;
default: return textComponent;
}
}
@@ -143,59 +113,16 @@ Rectangle {
}
}
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
}
QoAButton {
id: stopButtonId
anchors {
right: parent.right
top: parent.top
}
text: qsTr("ResetTo")
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
}
component TextComponent : TextBlock {
required property var itemData
height: implicitHeight + 10
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
: itemData.text
font.family: root.textFontFamily
font.pointSize: root.textFontSize
textFormat: {
if (root.textFormat == 0) {
return Text.MarkdownText
} else if (root.textFormat == 1) {
return Text.RichText
} else {
return Text.PlainText
}
}
ChatUtils {
id: utils
}
text: itemData.text
}
component CodeBlockComponent : CodeBlock {
id: codeblock
component CodeBlockComponent : CodeBlock {
required property var itemData
anchors {
left: parent.left
@@ -206,7 +133,5 @@ Rectangle {
code: itemData.text
language: itemData.language
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -22,8 +22,7 @@ import QtQuick.Controls
import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts
import ChatView
import UIControls
import Qt.labs.platform as Platform
import "./controls"
import "./parts"
ChatRootView {
@@ -71,17 +70,12 @@ ChatRootView {
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
tokensBadge {
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
text: qsTr("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
pinButton {
visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
}
ListView {
@@ -96,13 +90,14 @@ ChatRootView {
boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000
delegate: Loader {
delegate: ChatItem {
required property var model
required property int index
width: ListView.view.width - scroll.width
sourceComponent: model.roleType === ChatModel.Tool ? toolMessageComponent : chatItemComponent
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
color: model.roleType === ChatModel.User ? palette.alternateBase
: palette.base
}
header: Item {
@@ -123,36 +118,6 @@ ChatRootView {
root.scrollToBottom()
}
}
Component {
id: chatItemComponent
ChatItem {
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
isUserMessage: model.roleType === ChatModel.User
messageIndex: index
textFontFamily: root.textFontFamily
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
textFontSize: root.textFontSize
textFormat: root.textFormat
onResetChatToMessage: function(idx) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx)
}
}
}
Component {
id: toolMessageComponent
ToolStatusItem {
toolContent: model.content
}
}
}
ScrollView {
@@ -165,9 +130,7 @@ ChatRootView {
QQC.TextArea {
id: messageInput
placeholderText: Qt.platform.os === "osx"
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderText: qsTr("Type your message here...")
placeholderTextColor: palette.mid
color: palette.text
background: Rectangle {
@@ -190,53 +153,15 @@ ChatRootView {
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: messageContextMenu.open()
propagateComposedEvents: true
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
root.sendChatMessage()
event.accepted = true;
}
}
}
}
Platform.Menu {
id: messageContextMenu
Platform.MenuItem {
text: qsTr("Cut")
enabled: messageInput.selectedText.length > 0
onTriggered: messageInput.cut()
}
Platform.MenuItem {
text: qsTr("Copy")
enabled: messageInput.selectedText.length > 0
onTriggered: messageInput.copy()
}
Platform.MenuItem {
text: qsTr("Paste")
enabled: messageInput.canPaste
onTriggered: messageInput.paste()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("Select All")
enabled: messageInput.text.length > 0
onTriggered: messageInput.selectAll()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: qsTr("Clear")
enabled: messageInput.text.length > 0
onTriggered: messageInput.clear()
}
}
AttachedFilesPlace {
id: attachedFilesPlace
@@ -265,30 +190,16 @@ ChatRootView {
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
sendButton.onClicked: root.sendChatMessage()
stopButton.onClicked: root.cancelRequest()
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
}
attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog()
}
}
Shortcut {
id: sendMessageShortcut
sequence: "Ctrl+Return"
context: Qt.WindowShortcut
onActivated: {
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
root.sendChatMessage()
}
testRag.onClicked: root.testRAG(messageInput.text)
testChunks.onClicked: root.testChunking()
}
}
@@ -307,22 +218,4 @@ ChatRootView {
messageInput.text = ""
scrollToBottom()
}
ErrorToast {
id: errorToast
z: 1000
}
Connections {
target: root
function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage)
}
}
}
Component.onCompleted: {
messageInput.forceActiveFocus()
}
}

View File

@@ -1,159 +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/>.
*/
import QtQuick
import Qt.labs.platform as Platform
Rectangle {
id: root
property string toolContent: ""
property bool expanded: false
readonly property int firstNewline: toolContent.indexOf('\n')
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
radius: 6
color: palette.base
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
MouseArea {
id: header
width: parent.width
height: headerRow.height + 10
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
width: parent.width
spacing: 8
Text {
text: qsTr("Tool: %1").arg(root.toolName)
font.pixelSize: 13
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
Column {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
spacing: 8
TextEdit {
id: resultText
text: root.toolResult
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
enabled: resultText.selectedText.length > 0
onTriggered: resultText.copy()
}
Platform.MenuItem {
text: qsTr("Select All")
enabled: resultText.text.length > 0
onTriggered: resultText.selectAll()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: messageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
: Qt.lighter(palette.alternateBase, 1.3)
radius: root.radius
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: header.height
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + contentColumn.height + 20
}
}
]
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -20,127 +20,71 @@
import QtQuick
import QtQuick.Controls
import ChatView
import UIControls
import Qt.labs.platform as Platform
Rectangle {
id: root
property string code: ""
property string language: ""
property bool expanded: false
property alias codeFontFamily: codeText.font.family
property alias codeFontSize: codeText.font.pointSize
readonly property real collapsedHeight: copyButton.height + 10
readonly property string monospaceFont: {
switch (Qt.platform.os) {
case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
}
}
color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3)
border.width: 2
radius: 4
implicitWidth: parent.width
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
implicitWidth: parent.width
implicitHeight: codeText.implicitHeight + 20
ChatUtils {
id: utils
}
HoverHandler {
id: hoverHandler
enabled: true
}
MouseArea {
id: header
width: parent.width
height: root.collapsedHeight
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
spacing: 6
Text {
text: root.language ? qsTr("Code (%1)").arg(root.language) :
qsTr("Code")
font.pixelSize: 12
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
TextEdit {
id: codeText
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
anchors.fill: parent
anchors.margins: 10
text: root.code
readOnly: true
selectByMouse: true
font.family: root.monospaceFont
font.pointSize: Qt.application.font.pointSize
color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap
selectionColor: palette.highlight
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
}
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = codeText.selectedText || root.code
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
TextEdit {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: 5
readOnly: true
selectByMouse: true
text: root.language
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
: Qt.lighter(root.color, 1.1)
font.pointSize: 8
}
QoAButton {
id: copyButton
anchors.top: parent.top
anchors.right: parent.right
anchors.rightMargin: 5
y: 5
text: qsTr("Copy")
anchors.margins: 5
text: "Copy"
onClicked: {
utils.copyToClipboard(root.code)
text = qsTr("Copied")
@@ -153,21 +97,4 @@ Rectangle {
onTriggered: parent.text = qsTr("Copy")
}
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: root.collapsedHeight
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + codeText.implicitHeight + 10
}
}
]
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -18,7 +18,6 @@
*/
import QtQuick
import Qt.labs.platform as Platform
TextEdit {
id: root
@@ -26,29 +25,7 @@ TextEdit {
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
textFormat: Text.StyledText
selectionColor: palette.highlight
color: palette.text
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
enabled: root.selectedText.length > 0
onTriggered: root.copy()
}
Platform.MenuItem {
text: qsTr("Select All")
enabled: root.text.length > 0
onTriggered: root.selectAll()
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -21,16 +21,17 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
import UIControls
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId
property alias testRag: testRagId
property alias testChunks: testChunksId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@@ -52,12 +53,13 @@ Rectangle {
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
text: qsTr("Send")
}
QoAButton {
id: stopButtonId
text: qsTr("Stop")
}
QoAButton {
@@ -68,9 +70,7 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Attach file to message")
text: qsTr("Attach files")
}
QoAButton {
@@ -81,9 +81,7 @@ Rectangle {
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Link file to context")
text: qsTr("Link files")
}
CheckBox {
@@ -95,6 +93,18 @@ Rectangle {
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
}
QoAButton {
id: testRagId
text: qsTr("Test RAG")
}
QoAButton {
id: testChunksId
text: qsTr("Test Chunks")
}
Item {
Layout.fillWidth: true
}

View File

@@ -1,100 +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/>.
*/
import QtQuick
Rectangle {
id: errorToast
property string errorText: ""
property int displayDuration: 5000
width: Math.min(parent.width - 40, errorTextItem.implicitWidth + radius)
height: visible ? (errorTextItem.implicitHeight + 12) : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
color: "#d32f2f"
radius: height / 2
border.color: "#b71c1c"
border.width: 1
visible: false
opacity: 0
TextEdit {
id: errorTextItem
anchors.centerIn: parent
anchors.margins: 6
text: errorToast.errorText
color: "#ffffff"
font.pixelSize: 13
wrapMode: TextEdit.Wrap
width: Math.min(implicitWidth, errorToast.parent.width - 60)
horizontalAlignment: TextEdit.AlignHCenter
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: "#b71c1c"
}
function show(message) {
errorText = message
visible = true
showAnimation.start()
hideTimer.restart()
}
function hide() {
hideAnimation.start()
}
NumberAnimation {
id: showAnimation
target: errorToast
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
id: hideAnimation
target: errorToast
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InQuad
onFinished: errorToast.visible = false
}
Timer {
id: hideTimer
interval: errorToast.displayDuration
running: false
repeat: false
onTriggered: errorToast.hide()
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -19,9 +19,7 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import ChatView
import UIControls
Rectangle {
id: root
@@ -32,7 +30,6 @@ Rectangle {
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
@@ -49,61 +46,22 @@ Rectangle {
spacing: 10
QoAButton {
id: pinButtonId
checkable: true
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
QoAButton {
id: saveButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Save chat to *.json file")
text: qsTr("Save")
}
QoAButton {
id: loadButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Load chat from *.json file")
text: qsTr("Load")
}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
text: qsTr("Clear")
}
Text {
@@ -116,14 +74,7 @@ Rectangle {
QoAButton {
id: openChatHistoryId
icon {
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Show in system")
text: qsTr("Show in system")
}
Item {
@@ -132,10 +83,6 @@ Rectangle {
Badge {
id: tokensBadgeId
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
}
}
}

View File

@@ -1,6 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
/*
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -19,177 +18,41 @@
*/
#include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash>
namespace QodeAssist {
struct LanguageProperties
{
QString name;
QString commentStyle;
QVector<QString> namesFromModel;
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> customLanguagesFromSettings()
{
QVector<LanguageProperties> customLanguages;
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
for (const QString &entry : customLanguagesList) {
if (entry.trimmed().isEmpty()) {
continue;
}
QStringList parts = entry.split(',');
if (parts.size() < 4) {
continue;
}
QString name = parts[0].trimmed();
QString commentStyle = parts[1].trimmed();
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
&& !extensionsList.isEmpty()) {
QVector<QString> modelNames;
for (const auto &modelName : modelNamesList) {
modelNames.append(modelName);
}
QVector<QString> extensions;
for (const auto &ext : extensionsList) {
extensions.append(ext);
}
customLanguages.append({name, commentStyle, modelNames, extensions});
}
}
return customLanguages;
}
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
{"python", "#", {"python", "py"}, {"py"}},
{"lua", "--", {"lua"}, {"lua"}},
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
{"java", "//", {"java"}, {"java"}},
{"c#", "//", {"cs", "csharp"}, {"cs"}},
{"php", "//", {"php"}, {"php"}},
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
{"go", "//", {"go"}, {"go"}},
{"swift", "//", {"swift"}, {"swift"}},
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
{"scala", "//", {"scala"}, {"scala"}},
{"r", "#", {"r"}, {"r"}},
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
knownLanguages.append(customLanguagesFromSettings());
return knownLanguages;
}
bool CodeHandler::hasCodeBlocks(const QString &text)
{
QStringList lines = text.split('\n');
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
return true;
}
}
return false;
}
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
result[languageProps.name] = languageProps.commentStyle;
}
return result;
}
static QHash<QString, QString> buildExtensionToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &extension : languageProps.fileExtensions) {
result[extension] = languageProps.name;
}
}
return result;
}
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &nameFromModel : languageProps.namesFromModel) {
result[nameFromModel] = languageProps.name;
}
}
return result;
}
QString CodeHandler::processText(QString text, QString currentFilePath)
QString CodeHandler::processText(QString text)
{
QString result;
QStringList lines = text.split('\n');
bool inCodeBlock = false;
QString pendingComments;
auto currentFileExtension = QFileInfo(currentFilePath).suffix();
auto currentLanguage = detectLanguageFromExtension(currentFileExtension);
auto addPendingCommentsIfAny = [&]() {
if (pendingComments.isEmpty()) {
return;
}
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
pendingComments.clear();
};
QString currentLanguage;
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto lineLanguage = detectLanguageFromLine(line);
if (!lineLanguage.isEmpty()) {
currentLanguage = lineLanguage;
}
addPendingCommentsIfAny();
if (lineLanguage.isEmpty()) {
// language not detected, so add direct output from model, if any
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
}
currentLanguage = detectLanguage(line);
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
if (!pendingComments.isEmpty()) {
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
pendingComments.clear();
}
result += line + "\n";
} else {
QString trimmed = line.trimmed();
@@ -201,27 +64,45 @@ QString CodeHandler::processText(QString text, QString currentFilePath)
}
}
addPendingCommentsIfAny();
if (!pendingComments.isEmpty()) {
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
}
return result;
}
QString CodeHandler::getCommentPrefix(const QString &language)
{
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
return commentPrefixes.value(language, "//");
static const QHash<QString, QString> commentPrefixes
= {{"python", "#"}, {"py", "#"}, {"lua", "--"}, {"javascript", "//"},
{"js", "//"}, {"typescript", "//"}, {"ts", "//"}, {"cpp", "//"},
{"c++", "//"}, {"c", "//"}, {"java", "//"}, {"csharp", "//"},
{"cs", "//"}, {"php", "//"}, {"ruby", "#"}, {"rb", "#"},
{"rust", "//"}, {"rs", "//"}, {"go", "//"}, {"swift", "//"},
{"kotlin", "//"}, {"kt", "//"}, {"scala", "//"}, {"r", "#"},
{"shell", "#"}, {"bash", "#"}, {"sh", "#"}, {"perl", "#"},
{"pl", "#"}, {"haskell", "--"}, {"hs", "--"}};
return commentPrefixes.value(language.toLower(), "//");
}
QString CodeHandler::detectLanguageFromLine(const QString &line)
QString CodeHandler::detectLanguage(const QString &line)
{
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
}
QString trimmed = line.trimmed();
if (trimmed.length() <= 3) { // Если только ```
return QString();
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
return trimmed.mid(3).trimmed();
}
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -28,25 +28,11 @@ namespace QodeAssist {
class CodeHandler
{
public:
static QString processText(QString text, QString currentFileName);
/**
* Detects language from line, or returns empty string if this was not possible
*/
static QString detectLanguageFromLine(const QString &line);
/**
* Detects language file name, or returns empty string if this was not possible
*/
static QString detectLanguageFromExtension(const QString &extension);
/**
* Detects if text contains code blocks, or returns false if this was not possible
*/
static bool hasCodeBlocks(const QString &text);
static QString processText(QString text);
private:
static QString getCommentPrefix(const QString &language);
static QString detectLanguage(const QString &line);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer>
#include <settings/ButtonAspect.hpp>
#include "QodeAssisttr.h"
@@ -35,49 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -107,14 +64,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
}
void ConfigurationManager::selectProvider()
@@ -132,8 +81,10 @@ void ConfigurationManager::selectProvider()
: m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog(
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
m_generalSettings.showSelectionDialog(providersList,
targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
});
}
@@ -186,22 +137,19 @@ void ConfigurationManager::selectTemplate()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1
? m_templateManger.getFimTemplatesForProvider(providerID)
: m_templateManger.getChatTemplatesForProvider(providerID);
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog(
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
m_generalSettings.showSelectionDialog(templateList,
targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -36,11 +36,6 @@ public:
void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();
void selectModel();

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -23,37 +23,32 @@
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <llmcore/RequestConfig.hpp>
#include <texteditor/textdocument.h>
#include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "llmcore/MessageBuilder.hpp"
#include "llmcore/PromptTemplateManager.hpp"
#include "llmcore/ProvidersManager.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
namespace QodeAssist {
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger)
: m_generalSettings(generalSettings)
, m_completeSettings(completeSettings)
, m_providerRegistry(providerRegistry)
, m_promptProvider(promptProvider)
, m_documentReader(documentReader)
, m_performanceLogger(performanceLogger)
, m_contextManager(new Context::ContextManager(this))
LLMClientInterface::LLMClientInterface()
: m_requestHandler(this)
{
connect(&m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
&LLMClientInterface::sendCompletionToClient);
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "QodeAssist";
return "Qode Assist";
}
void LLMClientInterface::startImpl()
@@ -61,29 +56,6 @@ void LLMClientInterface::startImpl()
emit started();
}
void LLMClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
sendCompletionToClient(fullText, ctx.originalRequest, true);
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
m_activeRequests.erase(it);
}
void LLMClientInterface::sendData(const QByteArray &data)
{
QJsonDocument doc = QJsonDocument::fromJson(data);
@@ -103,7 +75,7 @@ void LLMClientInterface::sendData(const QByteArray &data)
handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId);
startTimeMeasurement(requestId);
handleCompletion(request);
} else if (method == "$/cancelRequest") {
handleCancelRequest(request);
@@ -116,16 +88,12 @@ void LLMClientInterface::sendData(const QByteArray &data)
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
{
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
QString id = request["params"].toObject()["id"].toString();
if (m_requestHandler.cancelRequest(id)) {
LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
} else {
LOG_MESSAGE(QString("Request %1 not found").arg(id));
}
m_activeRequests.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void LLMClientInterface::handleInitialize(const QJsonObject &request)
@@ -178,37 +146,43 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
emit finished();
}
bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
{
auto &generalSettings = Settings::generalSettings();
Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
}
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available for" + filePath);
return;
}
const auto updatedContext = prepareContext(request);
auto &completeSettings = Settings::codeCompletionSettings();
auto &generalSettings = Settings::generalSettings();
auto updatedContext = prepareContext(request, documentInfo);
bool isPreset1Active = isSpecifyCompletion(request);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
const auto providerName = !isPreset1Active ? generalSettings.ccProvider()
: generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? generalSettings.ccModel()
: generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? generalSettings.ccUrl() : generalSettings.ccPreset1Url();
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
: m_generalSettings.ccPreset1Provider();
const auto modelName = !isPreset1Active ? m_generalSettings.ccModel()
: m_generalSettings.ccPreset1Model();
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
: m_generalSettings.ccPreset1Url();
const auto provider = m_providerRegistry.getProviderByName(providerName);
const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return;
}
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
: generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -220,79 +194,44 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
config.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider;
config.promptTemplate = promptTemplate;
// TODO refactor networking
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
} else {
config.url = QUrl(
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
config.providerRequest = {{"model", modelName}, {"stream", true}};
}
config.url = QUrl(QString("%1%2").arg(
url,
promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
: provider->chatEndpoint()));
config.apiKey = provider->apiKey();
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
config.providerRequest = {{"model", modelName}, {"stream", completeSettings.stream()}};
config.multiLineCompletion = completeSettings.multiLineCompletion();
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == LLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
if (completeSettings.useSystemPrompt())
systemPrompt.append(completeSettings.systemPrompt());
if (!updatedContext.fileContext.isEmpty())
systemPrompt.append(updatedContext.fileContext);
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
QString userMessage;
if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
} else {
userMessage = updatedContext.prefix;
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
auto message = LLMCore::MessageBuilder()
.addSystemMessage(systemPrompt)
.addUserMessage(userMessage)
.addSuffix(updatedContext.suffix)
.addTokenizer(promptTemplate);
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == LLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<LLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) {
userMessage = m_completeSettings.processMessageToFIM(
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or("");
}
// TODO refactor add message
QVector<LLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
config.provider->prepareRequest(
message.saveTo(
config.providerRequest,
promptTemplate,
updatedContext,
LLMCore::RequestType::CodeCompletion);
providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
@@ -300,78 +239,62 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
LOG_MESSAGES(errors);
return;
}
QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId);
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
m_requestHandler.sendLLMRequest(config, request);
}
LLMCore::ContextData LLMClientInterface::prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QStringView &accumulatedCompletion)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return LLMCore::ContextData{};
}
int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
Context::DocumentContextReader reader(textDocument);
return reader.prepareContext(lineNumber, cursorPosition);
}
QString LLMClientInterface::endpoint(
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
{
QString endpoint;
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue()
: m_generalSettings.ccEndpointMode.stringValue();
if (endpointMode == "Auto") {
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
: provider->chatEndpoint();
} else if (endpointMode == "Custom") {
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
} else if (endpointMode == "FIM") {
endpoint = provider->completionEndpoint();
} else if (endpointMode == "Chat") {
endpoint = provider->chatEndpoint();
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
return Context::ProgrammingLanguage::Unknown;
}
return endpoint;
return Context::ProgrammingLanguageUtils::fromMimeType(textDocument->mimeType());
}
Context::ContextManager *LLMClientInterface::contextManager() const
void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request,
bool isComplete)
{
return m_contextManager;
}
bool isPreset1Active = isSpecifyCompletion(request);
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
: Settings::generalSettings().ccPreset1Template();
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
@@ -383,23 +306,11 @@ void LLMClientInterface::sendCompletionToClient(
QJsonArray completions;
QJsonObject completionItem;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
QString processedCompletion;
if (outputHandler == "Raw text") {
processedCompletion = completion;
} else if (outputHandler == "Force processing") {
processedCompletion = CodeHandler::processText(completion,
Context::extractFilePathFromRequest(request));
} else { // "Auto"
processedCompletion = CodeHandler::hasCodeBlocks(completion)
? CodeHandler::processText(completion,
Context::extractFilePathFromRequest(
request))
: completion;
}
QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
&& Settings::codeCompletionSettings().smartProcessInstuctText()
? CodeHandler::processText(completion)
: completion;
completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range;
@@ -418,13 +329,37 @@ void LLMClientInterface::sendCompletionToClient(
QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE(
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
LOG_MESSAGE(QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
}
void LLMClientInterface::startTimeMeasurement(const QString &requestId)
{
m_requestStartTimes[requestId] = QDateTime::currentMSecsSinceEpoch();
}
void LLMClientInterface::endTimeMeasurement(const QString &requestId)
{
if (m_requestStartTimes.contains(requestId)) {
qint64 startTime = m_requestStartTimes[requestId];
qint64 endTime = QDateTime::currentMSecsSinceEpoch();
qint64 totalTime = endTime - startTime;
logPerformance(requestId, "TotalCompletionTime", totalTime);
m_requestStartTimes.remove(requestId);
}
}
void LLMClientInterface::logPerformance(const QString &requestId,
const QString &operation,
qint64 elapsedMs)
{
LOG_MESSAGE(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -22,15 +22,9 @@
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
#include <llmcore/RequestHandler.hpp>
class QNetworkReply;
class QNetworkAccessManager;
@@ -42,32 +36,20 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT
public:
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
LLMClientInterface();
Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete);
void sendCompletionToClient(const QString &completion,
const QJsonObject &request,
bool isComplete);
void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected:
void startImpl() override;
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
void sendData(const QByteArray &data) override;
void parseCurrentMessage() override;
private:
void handleInitialize(const QJsonObject &request);
@@ -77,25 +59,18 @@ private:
void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request);
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
LLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify);
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
LLMCore::RequestHandler m_requestHandler;
QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests;
QMap<QString, qint64> m_requestStartTimes;
void startTimeMeasurement(const QString &requestId);
void endTimeMeasurement(const QString &requestId);
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
};
} // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -29,36 +29,6 @@
namespace QodeAssist {
QString mergeWithRightText(const QString &suggestion, const QString &rightText)
{
if (suggestion.isEmpty() || rightText.isEmpty()) {
return suggestion;
}
int j = 0;
QString processed = rightText;
QSet<int> matchedPositions;
for (int i = 0; i < suggestion.length() && j < processed.length(); ++i) {
if (suggestion[i] == processed[j]) {
matchedPositions.insert(j);
++j;
}
}
if (matchedPositions.isEmpty()) {
return suggestion + rightText;
}
QList<int> positions = matchedPositions.values();
std::sort(positions.begin(), positions.end(), std::greater<int>());
for (int pos : positions) {
processed.remove(pos, 1);
}
return suggestion;
}
LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
@@ -68,28 +38,21 @@ LLMSuggestion::LLMSuggestion(
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
int endPos = data.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(startPos, endPos, sourceDocument->characterCount());
startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
QTextBlock block = cursor.block();
QString blockText = block.text();
int cursorPositionInBlock = cursor.positionInBlock();
int startPosInBlock = startPos - block.position();
int endPosInBlock = endPos - block.position();
QString rightText = blockText.mid(cursorPositionInBlock);
if (!data.text.contains('\n')) {
QString processedRightText = mergeWithRightText(data.text, rightText);
processedRightText = processedRightText.mid(data.text.length());
QString displayText = blockText.left(cursorPositionInBlock) + data.text
+ processedRightText;
replacementDocument()->setPlainText(displayText);
} else {
QString displayText = blockText.left(cursorPositionInBlock) + data.text;
replacementDocument()->setPlainText(displayText);
}
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
replacementDocument()->setPlainText(blockText);
}
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
@@ -114,87 +77,31 @@ bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1) {
if (part == Line) {
next = text.length();
} else {
return apply();
}
}
if (next == -1)
return apply();
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty()) {
if (subText.isEmpty())
return false;
}
if (!subText.contains('\n')) {
currentCursor.insertText(subText);
currentCursor.insertText(subText);
const QString remainingText = text.mid(next);
if (!remainingText.isEmpty()) {
QTextCursor newCursor = widget->textCursor();
const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument(
newCursor.document(), newCursor.position());
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position
newEnd{newStart.line, newStart.column + int(remainingText.length())};
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
} else {
currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0};
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
}
}
return false;
}
bool LLMSuggestion::apply()
{
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
const QString text = suggestions()[currentSuggestion()].text;
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QTextCursor editCursor = cursor;
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd != -1) {
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedFirstLine = mergeWithRightText(firstLine, textAfterCursor);
editCursor.insertText(mergedFirstLine + restOfText);
} else {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
QString mergedText = mergeWithRightText(text, textAfterCursor);
editCursor.insertText(mergedText);
}
return true;
}
} // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -40,6 +40,5 @@ public:
bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
};
} // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*
@@ -68,10 +68,9 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public:
static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams(
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version,
const LanguageServerProtocol::Position &position)
GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
int version,
const LanguageServerProtocol::Position &position)
{
setTextDocument(document);
setVersion(version);

View File

@@ -1,14 +1,13 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.7.1",
"CompatVersion" : "${IDE_VERSION}",
"Version" : "0.4.13",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "",
${IDE_PLUGIN_DEPENDENCIES}
}

View File

@@ -2,13 +2,5 @@
<qresource prefix="/">
<file>resources/images/qoderassist-icon@2x.png</file>
<file>resources/images/qoderassist-icon.png</file>
<file>resources/images/repeat-last-instruct-icon@2x.png</file>
<file>resources/images/repeat-last-instruct-icon.png</file>
<file>resources/images/improve-current-code-icon@2x.png</file>
<file>resources/images/improve-current-code-icon.png</file>
<file>resources/images/suggest-new-icon.png</file>
<file>resources/images/suggest-new-icon@2x.png</file>
<file>resources/images/qode-assist-chat-icon.png</file>
<file>resources/images/qode-assist-chat-icon@2x.png</file>
</qresource>
</RCC>

View File

@@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
* This file is part of Qode Assist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,10 +24,8 @@
#include "QodeAssistClient.hpp"
#include <QInputDialog>
#include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h>
@@ -37,7 +35,6 @@
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
@@ -47,12 +44,11 @@ using namespace Core;
namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(new LLMClientInterface())
, m_recentCharCount(0)
{
setName("QodeAssist");
setName("Qode Assist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
@@ -132,13 +128,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
scheduleRequest(widget);
}
});
// auto editors = BaseTextEditor::textEditorsForDocument(document);
// connect(
// editors.first()->editorWidget(),
// &TextEditorWidget::selectionChanged,
// this,
// [this, editors]() { m_chatButtonHandler.showButton(editors.first()->editorWidget()); });
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -153,26 +142,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return;
const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
m_progressHandler.showProgress(editor);
}
GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return);
@@ -182,35 +159,6 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request);
}
void QodeAssistClient::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this);
connect(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
}
m_progressHandler.showProgress(editor);
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{
cancelRunningRequest(editor);
@@ -240,8 +188,8 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
}
void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
TextEditor::TextEditorWidget *editor)
{
if (response.error())
log(*response.error());
@@ -288,7 +236,6 @@ void QodeAssistClient::handleCompletions(
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
m_progressHandler.hideProgress();
if (completions.isEmpty())
return;
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
@@ -300,7 +247,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
cancelRequest(it->id());
m_runningRequests.erase(it);
}
@@ -321,13 +267,18 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument);
};
m_documentOpenedConnection
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
m_documentClosedConnection = connect(
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(document))
closeDocument(textDocument);
});
m_documentOpenedConnection = connect(EditorManager::instance(),
&EditorManager::documentOpened,
this,
openDoc);
m_documentClosedConnection = connect(EditorManager::instance(),
&EditorManager::documentClosed,
this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument);
});
for (IDocument *doc : DocumentModel::openedDocuments())
openDoc(doc);
@@ -342,32 +293,4 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear();
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
if (!result.success) {
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
auto editor = BaseTextEditor::currentTextEditor();
if (!editor) {
LOG_MESSAGE("Refactoring failed: No active editor found");
return;
}
auto editorWidget = editor->editorWidget();
QTextCursor cursor = editorWidget->textCursor();
cursor.beginEditBlock();
int startPos = result.insertRange.begin.toPositionInDocument(editorWidget->document());
int endPos = result.insertRange.end.toPositionInDocument(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
cursor.insertText(result.newText);
cursor.endEditBlock();
m_progressHandler.hideProgress();
}
} // namespace QodeAssist

View File

@@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
* This file is part of Qode Assist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,43 +24,32 @@
#pragma once
#include <QObject>
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include "LSPCompletion.hpp"
namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client
{
Q_OBJECT
public:
explicit QodeAssistClient(LLMClientInterface *clientInterface);
explicit QodeAssistClient();
~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override;
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
private:
void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void handleCompletions(const GetCompletionRequest::Response &response,
TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -69,10 +58,6 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

3
QodeAssist_en_001.ts Normal file
View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_001"></TS>

View File

@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of QodeAssist.
*

View File

@@ -1,331 +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 "QuickRefactorHandler.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
: QObject(parent)
, m_currentEditor(nullptr)
, m_isRefactoringInProgress(false)
, m_contextManager(this)
{
}
QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
if (m_isRefactoringInProgress) {
cancelRequest();
}
m_currentEditor = editor;
Utils::Text::Range range;
if (editor->textCursor().hasSelection()) {
QTextCursor cursor = editor->textCursor();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = editor->document()->findBlock(startPos);
int startLine = startBlock.blockNumber() + 1;
int startColumn = startPos - startBlock.position();
QTextBlock endBlock = editor->document()->findBlock(endPos);
int endLine = endBlock.blockNumber() + 1;
int endColumn = endPos - endBlock.position();
Utils::Text::Position startPosition;
startPosition.line = startLine;
startPosition.column = startColumn;
Utils::Text::Position endPosition;
endPosition.line = endLine;
endPosition.column = endColumn;
range = Utils::Text::Range();
range.begin = startPosition;
range.end = endPosition;
} else {
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
QTextBlock block = editor->document()->findBlock(cursorPos);
int line = block.blockNumber() + 1;
int column = cursorPos - block.position();
Utils::Text::Position cursorPosition;
cursorPosition.line = line;
cursorPosition.column = column;
range = Utils::Text::Range();
range.begin = cursorPosition;
range.end = cursorPosition;
}
m_currentRange = range;
prepareAndSendRequest(editor, instructions, range);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
const auto providerName = settings.caProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No provider found with name: %1").arg(providerName);
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.caTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
RefactorResult result;
result.success = false;
result.errorMessage = QString("No template found with name: %1").arg(templateName);
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.caUrl(), provider->chatEndpoint());
config.providerRequest = {{"model", settings.caModel()}, {"stream", true}};
config.apiKey = provider->apiKey();
LLMCore::ContextData context = prepareContext(editor, range, instructions);
provider
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_isRefactoringInProgress = true;
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&QuickRefactorHandler::handleRequestFailed,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
}
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
// TODO add selecting content before and after cursor/selection
QString fullContent = documentInfo.document->toPlainText();
QString taggedContent = fullContent;
if (cursor.hasSelection()) {
int selEnd = cursor.selectionEnd();
int selStart = cursor.selectionStart();
taggedContent
.insert(selEnd, selEnd == cursorPos ? "<selection_end><cursor>" : "<selection_end>");
taggedContent.insert(
selStart, selStart == cursorPos ? "<cursor><selection_start>" : "<selection_start>");
} else {
taggedContent.insert(cursorPos, "<cursor>");
}
QString systemPrompt = Settings::codeCompletionSettings().quickRefactorSystemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
project, LLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\nCode context with position markers:";
systemPrompt += taggedContent;
systemPrompt += "\n\nOutput format:";
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
"between<selection_start><selection_end> or be "
"inserted at cursor position<cursor>";
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
"code block markers";
systemPrompt += "\n- The output should be ready to insert directly into the editor";
systemPrompt += "\n- Follow the existing code style and indentation patterns";
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
QString cleanedResponse = response.trimmed();
if (cleanedResponse.startsWith("```")) {
int firstNewLine = cleanedResponse.indexOf('\n');
int lastFence = cleanedResponse.lastIndexOf("```");
if (firstNewLine != -1 && lastFence > firstNewLine) {
cleanedResponse
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
} else if (lastFence != -1) {
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
}
}
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::cancelRequest()
{
if (m_isRefactoringInProgress) {
auto id = m_lastRequestId;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.key() == id) {
const RequestContext &ctx = it.value();
ctx.provider->cancelRequest(id);
m_activeRequests.erase(it);
break;
}
}
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{
if (requestId == m_lastRequestId) {
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{
if (requestId == m_lastRequestId) {
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = error;
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

View File

@@ -1,88 +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 <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/Provider.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

229
README.md
View File

@@ -2,8 +2,7 @@
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-16.0.2-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-17.0.1-brightgreen)
![Static Badge](https://img.shields.io/badge/QtCreator-15.0.1-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
@@ -14,34 +13,36 @@
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## Table of Contents
1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
4. [Configure for OpenAI](#configure-for-openai)
5. [Configure for Mistral AI](#configure-for-mistral-ai)
6. [Configure for Google AI](#configure-for-google-ai)
7. [Configure for Ollama](#configure-for-ollama)
8. [Configure for llama.cpp](#configure-for-llamacpp)
9. [System Prompt Configuration](#system-prompt-configuration)
10. [File Context Feature](#file-context-feature)
11. [Quick Refactoring Feature](#quick-refactoring-feature)
12. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
13. [Development Progress](#development-progress)
14. [Hotkeys](#hotkeys)
15. [Ignoring Files](#ignoring-files)
14. [Troubleshooting](#troubleshooting)
15. [Support the Development](#support-the-development-of-qodeassist)
16. [How to Build](#how-to-build)
5. [Configure for using Ollama](#configure-for-using-ollama)
6. [System Prompt Configuration](#system-prompt-configuration)
7. [File Context Features](#file-context-features)
8. [Template-Model Compatibility](#template-model-compatibility)
9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
10. [Development Progress](#development-progress)
11. [Hotkeys](#hotkeys)
12. [Troubleshooting](#troubleshooting)
13. [Support the Development](#support-the-development-of-qodeassist)
14. [How to Build](#how-to-build)
## Overview
- AI-powered code completion
- Sharing IDE opened files with model context (disabled by default, need enable in settings)
- Quick refactor code via fast chat command and opened files
- Chat functionality:
- Side and Bottom panels(enabling in chat settings due stability reason with QQuickWidget problem)
- Chat in additional popup window with pinning(recommended)
- Side and Bottom panels
- Chat history autosave and restore
- Token usage monitoring and management
- Attach files for one-time code analysis
@@ -49,29 +50,19 @@
- Automatic syncing with open editor files (optional)
- Support for multiple LLM providers:
- Ollama
- llama.cpp
- OpenAI
- Anthropic Claude
- LM Studio
- Mistral AI
- Google AI
- OpenAI-compatible providers (eg. llama.cpp, https://openrouter.ai)
- OpenAI-compatible providers(eg. https://openrouter.ai)
- Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection
- Support tools/function calling (enabled by default)
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
<details>
<summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -87,27 +78,15 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details>
<details>
<summary>Chat in addtional window: (click to expand)</summary>
<img width="851" height="865" alt="image" src="https://github.com/user-attachments/assets/a68894b7-886e-4501-a61b-7161ae34b427" />
</details>
<details>
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
<details>
<summary>Example how tools works: (click to expand)</summary>
<img width="600" alt="ToolsDemo" src="https://github.com/user-attachments/assets/cf6273ad-d5c8-47fc-81e6-23d929547f6c">
</details>
## Install plugin to QtCreator
1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator
- Remove old version plugin if already was installed
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
@@ -141,33 +120,7 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
</details>
## Configure for Mistral AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Mistral AI api key
3. Return to General tab and configure:
- Set "Mistral AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.mistral.ai)
- Select your preferred model (e.g., mistral-large-latest)
- Choose the Mistral AI template for code completion or/and chat
<details>
<summary>Example of Mistral AI settings: (click to expand)</summary>
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
</details>
## Configure for Google AI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Google AI api key
3. Return to General tab and configure:
- Set "Google AI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
- Select your preferred model (e.g., gemini-2.0-flash)
- Choose the Google AI template
<details>
<summary>Example of Google AI settings: (click to expand)</summary>
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
</details>
## Configure for Ollama
## Configure for using Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
@@ -186,13 +139,12 @@ ollama run qwen2.5-coder:32b
```
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "QodeAssist" tab
2. Navigate to the "Qode Assist" tab
3. On the "General" page, verify:
- Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434
- Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
- Disable using tools if your model doesn't support tooling
4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator.
@@ -201,52 +153,11 @@ You're all set! QodeAssist is now ready to use in Qt Creator.
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
</details>
## Configure for llama.cpp
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to General tab and configure:
- Set "llama.cpp" as the provider for code completion or/and chat assistant
- Set the llama.cpp URL (e.g. http://localhost:8080)
- Fill in model name
- Choose template for model(e.g. llama.cpp FIM for any model with FIM support)
- Disable using tools if your model doesn't support tooling
<details>
<summary>Example of llama.cpp settings: (click to expand)</summary>
<img width="829" alt="llama.cpp Settings" src="https://github.com/user-attachments/assets/8c75602c-60f3-49ed-a7a9-d3c972061ea2" />
</details>
## System Prompt Configuration
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
## Project Rules Configuration
QodeAssist supports project-specific rules to customize AI behavior for your codebase. Create a `.qodeassist/rules/` directory in your project root.
### Quick Start
```bash
mkdir -p .qodeassist/rules/{common,completion,chat,quickrefactor}
```
```
.qodeassist/
└── rules/
├── common/ # Applied to all contexts
├── completion/ # Code completion only
├── chat/ # Chat assistant only
└── quickrefactor/ # Quick refactor only
```
All .md files in each directory are automatically loaded and added to the system prompt.
Example
Create .qodeassist/rules/common/general.md:
```markdown
# Project Guidelines
- Use snake_case for private members
- Prefix interfaces with 'I'
- Always document public APIs
- Prefer Qt containers over STL
```
## File Context Feature
## File Context Features
QodeAssist provides two powerful ways to include source code files in your chat conversations: Attachments and Linked Files. Each serves a distinct purpose and helps provide better context for the AI assistant.
@@ -279,25 +190,25 @@ Linked files provide persistent context throughout the conversation:
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## Quick Refactoring Feature
### Setup
Since this is actually a small chat with redirected output, the main settings of the provider, model and template are taken from the chat settings
### Using
The request to model consist of instructions to model, selection code and cursor position
The default instruction is: "Refactor the code to improve its quality and maintainability." and sending if text field is empty
Also there buttons to quick call instractions:
* Repeat latest instruction, will activate after sending first request in QtCreator session
* Improve current selection code
* Suggestion alternative variant of selection code
* Other instructions[TBD]
## Template-Model Compatibility
| Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion |
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance |
## QtCreator Version Compatibility
- QtCreator 17.0.0 - 0.6.0 - 0.x.x
- QtCreator 16.0.2 - 0.5.13 - 0.x.x
- QtCreator 16.0.1 - 0.5.7 - 0.5.13
- QtCreator 16.0.0 - 0.5.2 - 0.5.6
- QtCreator 15.0.1 - 0.4.8 - 0.5.1
- QtCreator 15.0.1 - 0.4.8 - 0.4.x
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below
@@ -310,53 +221,14 @@ Linked files provide persistent context throughout the conversation:
- [x] Sharing diff with model
- [ ] Sharing project source with model
- [ ] Support for more providers and models
- [ ] Support MCP
## Hotkeys
All hotkeys available in QtCreator Settings
Also you can find default hotkeys here:
- To call chat with llm in separate window, you can use:
- on Mac: Option + Command + W
- on Windows: Ctrl + Alt + W
- on Linux: Ctrl + Alt + W
- To close chat with llm in separate window, you can use:
- on Mac: Option + Command + S
- on Windows: Ctrl + Alt + S
- on Linux: Ctrl + Alt + S
- To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q
- on Linux with KDE Plasma: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
- To call Quick Refactor dialog, select some code or place cursor and press
- on Mac: Option + Command + R
- on Windows: Ctrl + Alt + R
- on Linux with KDE Plasma: Ctrl + Alt + R
## Ignoring Files
QodeAssist supports the ability to ignore files in context using a .qodeassistignore file. This allows you to exclude specific files from the context during code completion and in the chat assistant, which is especially useful for large projects.
### How to Use .qodeassistignore
- Create a .qodeassistignore file in the root directory of your project near CMakeLists.txt or pro.
- Add patterns for files and directories that should be excluded from the context.
- QodeAssist will automatically detect this file and apply the exclusion rules.
### .qodeassistignore File Format
The file format is similar to .gitignore:
- Each pattern is written on a separate line
- Empty lines are ignored
- Lines starting with # are considered comments
- Standard wildcards work the same as in .gitignore
- To negate a pattern, use ! at the beginning of the line
```
# Ignore all files in the build directory
/build
*.tmp
# Ignore a specific file
src/generated/autogen.cpp
```
## Troubleshooting
@@ -367,17 +239,20 @@ If QodeAssist is having problems connecting to the LLM provider, please check th
- For Ollama, the default is usually http://localhost:11434
- For LM Studio, the default is usually http://localhost:1234
2. Confirm that the selected model and template are compatible:
2. Check the endpoint:
Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using
Make sure the endpoint in the settings matches the one required by your provider
- For Ollama, it should be /api/generate
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
3. On Linux the prebuilt binaries support only ubuntu 22.04+ or simililliar os.
If you need compatiblity with another os, you have to build manualy. our experiments and resolution you can check here: https://github.com/Palm1r/QodeAssist/issues/48
3. Confirm that the selected model and template are compatible:
Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings
2. Navigate to the "QodeAssist" tab
2. Navigate to the "Qode Assist" tab
3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button
- The API key will not reset

View File

@@ -1,14 +0,0 @@
add_subdirectory(core)
add_subdirectory(Editor)
# add_subdirectory(serialization)
# add_subdirectory(tasks)
qt_add_library(TaskFlow STATIC)
target_link_libraries(TaskFlow
PUBLIC
TaskFlowCore
TaskFlowEditorplugin
# TaskFlowSerialization
# TaskFlowTasks
)

View File

@@ -1,41 +0,0 @@
qt_add_library(TaskFlowEditor STATIC)
qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
qt_add_qml_module(TaskFlowEditor
URI TaskFlow.Editor
VERSION 1.0
DEPENDENCIES QtQuick
RESOURCES
QML_FILES
qml/FlowEditorView.qml
qml/Flow.qml
qml/Task.qml
qml/TaskPort.qml
qml/TaskParameter.qml
qml/TaskConnection.qml
SOURCES
FlowEditor.hpp FlowEditor.cpp
FlowsModel.hpp FlowsModel.cpp
TaskItem.hpp TaskItem.cpp
FlowItem.hpp FlowItem.cpp
TaskModel.hpp TaskModel.cpp
TaskPortItem.hpp TaskPortItem.cpp
TaskPortModel.hpp TaskPortModel.cpp
TaskConnectionsModel.hpp TaskConnectionsModel.cpp
TaskConnectionItem.hpp TaskConnectionItem.cpp
GridBackground.hpp GridBackground.cpp
)
target_link_libraries(TaskFlowEditor
PUBLIC
Qt::Quick
PRIVATE
TaskFlowCore
)
target_include_directories(TaskFlowEditor
PUBLIC
${CMAKE_CURRENT_LIST_DIR}
)

View File

@@ -1,120 +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:
*/
#include "FlowEditor.hpp"
namespace QodeAssist::TaskFlow {
FlowEditor::FlowEditor(QQuickItem *parent)
: QQuickItem(parent)
{}
void FlowEditor::initialize()
{
emit availableTaskTypesChanged();
emit availableFlowsChanged();
m_flowsModel = new FlowsModel(m_flowManager, this);
emit flowsModelChanged();
if (m_flowsModel->rowCount() > 0) {
setCurrentFlowIndex(0);
}
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
m_currentFlow = m_flowManager->getFlow();
emit currentFlowChanged();
}
QString FlowEditor::currentFlowId() const
{
return m_currentFlowId;
}
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
{
if (m_currentFlowId == newCurrentFlowId)
return;
m_currentFlowId = newCurrentFlowId;
emit currentFlowIdChanged();
}
QStringList FlowEditor::availableTaskTypes() const
{
if (m_flowManager)
return m_flowManager->getAvailableTasksTypes();
else {
return {"No flow manager"};
}
}
QStringList FlowEditor::availableFlows() const
{
if (m_flowManager) {
auto flows = m_flowManager->getAvailableFlows();
return flows.size() > 0 ? flows : QStringList{"No flows"};
} else {
return {"No flow manager"};
}
}
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
{
if (m_flowManager == newFlowManager)
return;
m_flowManager = newFlowManager;
initialize();
}
FlowsModel *FlowEditor::flowsModel() const
{
return m_flowsModel;
}
int FlowEditor::currentFlowIndex() const
{
return m_currentFlowIndex;
}
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
{
if (m_currentFlowIndex == newCurrentFlowIndex)
return;
m_currentFlowIndex = newCurrentFlowIndex;
emit currentFlowIndexChanged();
}
Flow *FlowEditor::getFlow(const QString &flowName)
{
return m_flowManager->getFlow(flowName);
}
Flow *FlowEditor::getCurrentFlow()
{
return m_flowManager->getFlow(m_currentFlowId);
}
Flow *FlowEditor::currentFlow() const
{
return m_currentFlow;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,86 +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:
*/
#pragma once
#include <QQuickItem>
#include "FlowsModel.hpp"
#include <FlowManager.hpp>
namespace QodeAssist::TaskFlow {
class FlowEditor : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
Q_PROPERTY(
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
currentFlowIndexChanged)
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
public:
FlowEditor(QQuickItem *parent = nullptr);
void initialize();
QString currentFlowId() const;
void setCurrentFlowId(const QString &newCurrentFlowId);
QStringList availableTaskTypes() const;
QStringList availableFlows() const;
void setFlowManager(FlowManager *newFlowManager);
FlowsModel *flowsModel() const;
int currentFlowIndex() const;
void setCurrentFlowIndex(int newCurrentFlowIndex);
Q_INVOKABLE Flow *getFlow(const QString &flowName);
Q_INVOKABLE Flow *getCurrentFlow();
Flow *currentFlow() const;
signals:
void currentFlowIdChanged();
void availableTaskTypesChanged();
void availableFlowsChanged();
void flowsModelChanged();
void currentFlowIndexChanged();
void currentFlowChanged();
private:
FlowManager *m_flowManager = nullptr;
QString m_currentFlowId;
FlowsModel *m_flowsModel;
int m_currentFlowIndex;
Flow *m_currentFlow = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,90 +0,0 @@
#include "FlowItem.hpp"
namespace QodeAssist::TaskFlow {
FlowItem::FlowItem(QQuickItem *parent)
: QQuickItem(parent)
{
connect(this, &QQuickItem::childrenChanged, this, [this]() { updateFlowLayout(); });
}
QString FlowItem::flowId() const
{
if (!m_flow)
return {"no flow"};
return m_flow->flowId();
}
void FlowItem::setFlowId(const QString &newFlowId)
{
if (m_flow->flowId() == newFlowId)
return;
m_flow->setFlowId(newFlowId);
emit flowIdChanged();
}
Flow *FlowItem::flow() const
{
return m_flow;
}
void FlowItem::setFlow(Flow *newFlow)
{
if (m_flow == newFlow)
return;
m_flow = newFlow;
emit flowChanged();
emit flowIdChanged();
qDebug() << "FlowItem::setFlow" << m_flow->flowId() << newFlow;
m_taskModel = new TaskModel(m_flow, this);
m_connectionsModel = new TaskConnectionsModel(m_flow, this);
emit taskModelChanged();
emit connectionsModelChanged();
}
TaskModel *FlowItem::taskModel() const
{
return m_taskModel;
}
TaskConnectionsModel *FlowItem::connectionsModel() const
{
return m_connectionsModel;
}
QVariantList FlowItem::taskItems() const
{
return m_taskItems;
}
void FlowItem::setTaskItems(const QVariantList &newTaskItems)
{
qDebug() << "FlowItem::setTaskItems" << newTaskItems;
if (m_taskItems == newTaskItems)
return;
m_taskItems = newTaskItems;
emit taskItemsChanged();
}
void FlowItem::updateFlowLayout()
{
auto allItems = this->childItems();
for (auto child : allItems) {
if (child->objectName() == QString("TaskItem")) {
qDebug() << "Found TaskItem:" << child;
auto taskItem = qobject_cast<TaskItem *>(child);
m_taskItemsList.insert(taskItem, taskItem->task());
}
if (child->objectName() == QString("TaskConnectionItem")) {
qDebug() << "Found TaskConnectionItem:" << child;
auto connectionItem = qobject_cast<TaskConnectionItem *>(child);
m_taskConnectionsList.insert(connectionItem, connectionItem->connection());
}
}
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,61 +0,0 @@
#pragma once
#include <QQuickItem>
#include "TaskConnectionItem.hpp"
#include "TaskConnectionsModel.hpp"
#include "TaskItem.hpp"
#include "TaskModel.hpp"
#include <Flow.hpp>
#include <TaskConnection.hpp>
namespace QodeAssist::TaskFlow {
class FlowItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString flowId READ flowId WRITE setFlowId NOTIFY flowIdChanged)
Q_PROPERTY(Flow *flow READ flow WRITE setFlow NOTIFY flowChanged)
Q_PROPERTY(TaskModel *taskModel READ taskModel NOTIFY taskModelChanged)
Q_PROPERTY(
TaskConnectionsModel *connectionsModel READ connectionsModel NOTIFY connectionsModelChanged)
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
public:
explicit FlowItem(QQuickItem *parent = nullptr);
QString flowId() const;
void setFlowId(const QString &newFlowId);
Flow *flow() const;
void setFlow(Flow *newFlow);
TaskModel *taskModel() const;
TaskConnectionsModel *connectionsModel() const;
QVariantList taskItems() const;
void setTaskItems(const QVariantList &newTaskItems);
void updateFlowLayout();
signals:
void flowIdChanged();
void flowChanged();
void taskModelChanged();
void connectionsModelChanged();
void taskItemsChanged();
private:
Flow *m_flow = nullptr;
TaskModel *m_taskModel = nullptr;
TaskConnectionsModel *m_connectionsModel = nullptr;
QVariantList m_taskItems;
QHash<TaskItem *, BaseTask *> m_taskItemsList;
QHash<TaskConnectionItem *, TaskConnection *> m_taskConnectionsList;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,54 +0,0 @@
#include "FlowsModel.hpp"
#include "FlowManager.hpp"
namespace QodeAssist::TaskFlow {
FlowsModel::FlowsModel(FlowManager *flowManager, QObject *parent)
: QAbstractListModel(parent)
, m_flowManager(flowManager)
{
connect(m_flowManager, &FlowManager::flowAdded, this, &FlowsModel::onFlowAdded);
}
int FlowsModel::rowCount(const QModelIndex &parent) const
{
return m_flowManager->flows().size();
}
QVariant FlowsModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_flowManager || index.row() >= m_flowManager->flows().size())
return QVariant();
const auto flows = m_flowManager->flows().values();
switch (role) {
case FlowRoles::FlowIdRole:
return flows.at(index.row())->flowId();
case FlowRoles::FlowDataRole:
return QVariant::fromValue(flows.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> FlowsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[FlowRoles::FlowIdRole] = "flowId";
roles[FlowRoles::FlowDataRole] = "flowData";
return roles;
}
void FlowsModel::onFlowAdded(const QString &flowId)
{
// qDebug() << "FlowsModel::Flow added: " << flowId;
// int newIndex = m_flowManager->flows().size();
// beginInsertRows(QModelIndex(), newIndex, newIndex);
// endInsertRows();
}
void FlowsModel::onFlowRemoved(const QString &flowId) {}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,31 +0,0 @@
#pragma once
#include <QAbstractListModel>
#include <QObject>
// #include "tasks/Flow.hpp"
#include <FlowManager.hpp>
namespace QodeAssist::TaskFlow {
class FlowsModel : public QAbstractListModel
{
Q_OBJECT
public:
enum FlowRoles { FlowIdRole = Qt::UserRole, FlowDataRole };
FlowsModel(FlowManager *flowManager, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
public slots:
void onFlowAdded(const QString &flowId);
void onFlowRemoved(const QString &flowId);
private:
FlowManager *m_flowManager;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,98 +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 "GridBackground.hpp"
#include <QPainter>
#include <QPixmap>
#include <QQuickWindow>
#include <QSGSimpleRectNode>
#include <QSGSimpleTextureNode>
namespace QodeAssist::TaskFlow {
GridBackground::GridBackground(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(QQuickItem::ItemHasContents, true);
}
int GridBackground::gridSize() const
{
return m_gridSize;
}
void GridBackground::setGridSize(int size)
{
if (m_gridSize != size) {
m_gridSize = size;
update();
emit gridSizeChanged();
}
}
QColor GridBackground::gridColor() const
{
return m_gridColor;
}
void GridBackground::setGridColor(const QColor &color)
{
if (m_gridColor != color) {
m_gridColor = color;
update();
emit gridColorChanged();
}
}
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
if (!node) {
node = new QSGSimpleTextureNode();
}
QPixmap pixmap(width(), height());
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, false);
QPen pen(m_gridColor);
pen.setWidth(1);
painter.setPen(pen);
painter.setOpacity(this->opacity());
for (int x = 0; x < width(); x += m_gridSize) {
painter.drawLine(x, 0, x, height());
}
for (int y = 0; y < height(); y += m_gridSize) {
painter.drawLine(0, y, width(), y);
}
painter.end();
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
node->setTexture(texture);
node->setRect(boundingRect());
return node;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,57 +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 <QColor>
#include <QPainter>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class GridBackground : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
public:
explicit GridBackground(QQuickItem *parent = nullptr);
int gridSize() const;
void setGridSize(int size);
QColor gridColor() const;
void setGridColor(const QColor &color);
signals:
void gridSizeChanged();
void gridColorChanged();
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
private:
int m_gridSize = 20;
QColor m_gridColor = QColor(128, 128, 128);
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,153 +0,0 @@
#include "TaskConnectionItem.hpp"
#include "TaskItem.hpp"
#include "TaskPortItem.hpp"
#include <QDebug>
namespace QodeAssist::TaskFlow {
TaskConnectionItem::TaskConnectionItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskConnectionItem");
}
void TaskConnectionItem::setConnection(TaskConnection *connection)
{
if (m_connection == connection)
return;
m_connection = connection;
emit connectionChanged();
calculatePositions();
}
void TaskConnectionItem::updatePositions()
{
// calculatePositions();
}
void TaskConnectionItem::calculatePositions()
{
if (!m_connection) {
return;
}
// Find source task item
QQuickItem *sourceTaskItem = findTaskItem(m_connection->sourceTask());
QQuickItem *targetTaskItem = findTaskItem(m_connection->targetTask());
if (!sourceTaskItem || !targetTaskItem) {
return;
}
// Find port items within tasks
QQuickItem *sourcePortItem = findPortItem(sourceTaskItem, m_connection->sourcePort());
QQuickItem *targetPortItem = findPortItem(targetTaskItem, m_connection->targetPort());
if (!sourcePortItem || !targetPortItem) {
return;
}
// Calculate global positions
QPointF sourceGlobal
= sourcePortItem
->mapToItem(parentItem(), sourcePortItem->width() / 2, sourcePortItem->height() / 2);
QPointF targetGlobal
= targetPortItem
->mapToItem(parentItem(), targetPortItem->width() / 2, targetPortItem->height() / 2);
if (m_startPoint != sourceGlobal) {
m_startPoint = sourceGlobal;
emit startPointChanged();
}
if (m_endPoint != targetGlobal) {
m_endPoint = targetGlobal;
emit endPointChanged();
}
}
QQuickItem *TaskConnectionItem::findTaskItem(BaseTask *task)
{
for (const QVariant &item : m_taskItems) {
QQuickItem *taskItem = qvariant_cast<QQuickItem *>(item);
if (!taskItem)
continue;
QVariant taskProp = taskItem->property("task");
if (taskProp.isValid() && taskProp.value<BaseTask *>() == task) {
return taskItem;
}
}
return nullptr;
}
QQuickItem *TaskConnectionItem::findTaskItemRecursive(QQuickItem *item, BaseTask *task)
{
// Проверяем objectName и task property
if (item->objectName() == "TaskItem") {
QVariant taskProp = item->property("task");
if (taskProp.isValid()) {
BaseTask *itemTask = taskProp.value<BaseTask *>();
if (itemTask == task) {
return item;
}
}
}
// Рекурсивно ищем в детях
auto children = item->childItems();
for (QQuickItem *child : children) {
if (QQuickItem *found = findTaskItemRecursive(child, task)) {
return found;
}
}
return nullptr;
}
QQuickItem *TaskConnectionItem::findPortItem(QQuickItem *taskItem, TaskPort *port)
{
std::function<QQuickItem *(QQuickItem *)> findPortRecursive =
[&](QQuickItem *item) -> QQuickItem * {
// Проверяем objectName и port property
if (item->objectName() == "TaskPortItem") {
QVariant portProp = item->property("port");
if (portProp.isValid()) {
TaskPort *itemPort = portProp.value<TaskPort *>();
if (itemPort == port) {
return item;
}
}
}
// Рекурсивно ищем в детях
for (QQuickItem *child : item->childItems()) {
if (QQuickItem *found = findPortRecursive(child)) {
return found;
}
}
return nullptr;
};
return findPortRecursive(taskItem);
}
QVariantList TaskConnectionItem::taskItems() const
{
return m_taskItems;
}
void TaskConnectionItem::setTaskItems(const QVariantList &newTaskItems)
{
if (m_taskItems == newTaskItems)
return;
m_taskItems = newTaskItems;
emit taskItemsChanged();
calculatePositions();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,55 +0,0 @@
#pragma once
#include "TaskConnection.hpp"
#include <QPointF>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class TaskConnectionItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QPointF startPoint READ startPoint NOTIFY startPointChanged)
Q_PROPERTY(QPointF endPoint READ endPoint NOTIFY endPointChanged)
Q_PROPERTY(
TaskConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged)
Q_PROPERTY(QVariantList taskItems READ taskItems WRITE setTaskItems NOTIFY taskItemsChanged)
public:
TaskConnectionItem(QQuickItem *parent = nullptr);
QPointF startPoint() const { return m_startPoint; }
QPointF endPoint() const { return m_endPoint; }
TaskConnection *connection() const { return m_connection; }
void setConnection(TaskConnection *connection);
Q_INVOKABLE void updatePositions();
QVariantList taskItems() const;
void setTaskItems(const QVariantList &newTaskItems);
signals:
void startPointChanged();
void endPointChanged();
void connectionChanged();
void taskItemsChanged();
private:
void calculatePositions();
QQuickItem *findTaskItem(BaseTask *task);
QQuickItem *findTaskItemRecursive(QQuickItem *item, BaseTask *task);
QQuickItem *findPortItem(QQuickItem *taskItem, TaskPort *port);
private:
TaskConnection *m_connection = nullptr;
QPointF m_startPoint;
QPointF m_endPoint;
QVariantList m_taskItems;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +0,0 @@
#include "TaskConnectionsModel.hpp"
namespace QodeAssist::TaskFlow {
TaskConnectionsModel::TaskConnectionsModel(Flow *flow, QObject *parent)
: QAbstractListModel(parent)
, m_flow(flow)
{}
int TaskConnectionsModel::rowCount(const QModelIndex &parent) const
{
return m_flow->connections().size();
}
QVariant TaskConnectionsModel::data(const QModelIndex &index, int role) const
{
if (role == TaskConnectionsRoles::TaskConnectionsRole)
return QVariant::fromValue(m_flow->connections().at(index.row()));
return QVariant();
}
QHash<int, QByteArray> TaskConnectionsModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskConnectionsRoles::TaskConnectionsRole] = "connectionData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,25 +0,0 @@
#pragma once
#include <QAbstractListModel>
#include <QObject>
#include <Flow.hpp>
namespace QodeAssist::TaskFlow {
class TaskConnectionsModel : public QAbstractListModel
{
public:
enum TaskConnectionsRoles { TaskConnectionsRole = Qt::UserRole };
explicit TaskConnectionsModel(Flow *flow, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,69 +0,0 @@
#include "TaskItem.hpp"
namespace QodeAssist::TaskFlow {
TaskItem::TaskItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskItem");
}
QString TaskItem::taskId() const
{
return m_taskId;
}
void TaskItem::setTaskId(const QString &newTaskId)
{
if (m_taskId == newTaskId)
return;
m_taskId = newTaskId;
emit taskIdChanged();
}
QString TaskItem::taskType() const
{
return m_task ? m_task->taskType() : QString();
}
BaseTask *TaskItem::task() const
{
return m_task;
}
void TaskItem::setTask(BaseTask *newTask)
{
if (m_task == newTask)
return;
m_task = newTask;
if (m_task) {
m_taskId = m_task->taskId();
// Обновляем модели портов
m_inputPorts = new TaskPortModel(m_task->getInputPorts(), this);
m_outputPorts = new TaskPortModel(m_task->getOutputPorts(), this);
} else {
m_inputPorts = nullptr;
m_outputPorts = nullptr;
}
emit taskChanged();
emit inputPortsChanged();
emit outputPortsChanged();
emit taskIdChanged();
emit taskTypeChanged();
}
TaskPortModel *TaskItem::inputPorts() const
{
return m_inputPorts;
}
TaskPortModel *TaskItem::outputPorts() const
{
return m_outputPorts;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,49 +0,0 @@
#pragma once
#include <QQuickItem>
#include "TaskPortModel.hpp"
#include <BaseTask.hpp>
#include <TaskPort.hpp>
namespace QodeAssist::TaskFlow {
class TaskItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QString taskId READ taskId WRITE setTaskId NOTIFY taskIdChanged)
Q_PROPERTY(QString taskType READ taskType NOTIFY taskTypeChanged)
Q_PROPERTY(BaseTask *task READ task WRITE setTask NOTIFY taskChanged)
Q_PROPERTY(TaskPortModel *inputPorts READ inputPorts NOTIFY inputPortsChanged)
Q_PROPERTY(TaskPortModel *outputPorts READ outputPorts NOTIFY outputPortsChanged)
public:
TaskItem(QQuickItem *parent = nullptr);
QString taskId() const;
void setTaskId(const QString &newTaskId);
QString taskType() const;
BaseTask *task() const;
void setTask(BaseTask *newTask);
TaskPortModel *inputPorts() const;
TaskPortModel *outputPorts() const;
signals:
void taskIdChanged();
void taskTypeChanged();
void taskChanged();
void inputPortsChanged();
void outputPortsChanged();
private:
QString m_taskId;
BaseTask *m_task = nullptr;
TaskPortModel *m_inputPorts = nullptr;
TaskPortModel *m_outputPorts = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,40 +0,0 @@
#include "TaskModel.hpp"
namespace QodeAssist::TaskFlow {
TaskModel::TaskModel(Flow *flow, QObject *parent)
: QAbstractListModel(parent)
, m_flow(flow)
{}
int TaskModel::rowCount(const QModelIndex &parent) const
{
return m_flow->tasks().size();
}
QVariant TaskModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || !m_flow || index.row() >= m_flow->tasks().size())
return QVariant();
const auto &task = m_flow->tasks().values();
switch (role) {
case TaskRoles::TaskIdRole:
return task.at(index.row())->taskId();
case TaskRoles::TaskDataRole:
return QVariant::fromValue(task.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> TaskModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskRoles::TaskIdRole] = "taskId";
roles[TaskRoles::TaskDataRole] = "taskData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,25 +0,0 @@
#pragma once
#include <QAbstractListModel>
#include <Flow.hpp>
namespace QodeAssist::TaskFlow {
class TaskModel : public QAbstractListModel
{
Q_OBJECT
public:
enum TaskRoles { TaskIdRole = Qt::UserRole, TaskDataRole };
TaskModel(Flow *flow, QObject *parent);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
Flow *m_flow;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +0,0 @@
#include "TaskPortItem.hpp"
namespace QodeAssist::TaskFlow {
TaskPortItem::TaskPortItem(QQuickItem *parent)
: QQuickItem(parent)
{
setObjectName("TaskPortItem");
}
TaskPort *TaskPortItem::port() const
{
return m_port;
}
void TaskPortItem::setPort(TaskPort *newPort)
{
if (m_port == newPort)
return;
m_port = newPort;
emit portChanged();
}
QString TaskPortItem::name() const
{
return m_port->name();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,31 +0,0 @@
#pragma once
#include <TaskPort.hpp>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class TaskPortItem : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(TaskPort *port READ port WRITE setPort NOTIFY portChanged)
Q_PROPERTY(QString name READ name CONSTANT)
public:
TaskPortItem(QQuickItem *parent = nullptr);
TaskPort *port() const;
void setPort(TaskPort *newPort);
QString name() const;
signals:
void portChanged();
private:
TaskPort *m_port = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,39 +0,0 @@
#include "TaskPortModel.hpp"
#include "TaskPort.hpp"
namespace QodeAssist::TaskFlow {
TaskPortModel::TaskPortModel(const QList<TaskPort *> &ports, QObject *parent)
: QAbstractListModel(parent)
, m_ports(ports)
{}
int TaskPortModel::rowCount(const QModelIndex &parent) const
{
return m_ports.size();
}
QVariant TaskPortModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_ports.size())
return QVariant();
switch (role) {
case TaskPortRoles::TaskPortNameRole:
return m_ports.at(index.row())->name();
case TaskPortRoles::TaskPortDataRole:
return QVariant::fromValue(m_ports.at(index.row()));
default:
return QVariant();
}
}
QHash<int, QByteArray> TaskPortModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[TaskPortRoles::TaskPortNameRole] = "taskPortName";
roles[TaskPortRoles::TaskPortDataRole] = "taskPortData";
return roles;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,25 +0,0 @@
#pragma once
#include <QAbstractListModel>
#include <BaseTask.hpp>
namespace QodeAssist::TaskFlow {
class TaskPortModel : public QAbstractListModel
{
Q_OBJECT
public:
enum TaskPortRoles { TaskPortNameRole = Qt::UserRole, TaskPortDataRole };
TaskPortModel(const QList<TaskPort *> &ports, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
private:
QList<TaskPort *> m_ports;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,134 +0,0 @@
import QtQuick
import TaskFlow.Editor
FlowItem {
id: root
Repeater {
id: tasks
model: root.taskModel
delegate: Task {
// task: taskData
}
}
Repeater {
id: connections
model: root.taskModel
delegate: TaskConnection {
// task: taskData
}
}
// property var qtaskItems: []
// // Flow container background
// Rectangle {
// anchors.fill: parent
// color: palette.alternateBase
// border.color: palette.mid
// border.width: 2
// radius: 8
// // Flow header
// Rectangle {
// id: flowHeader
// anchors.top: parent.top
// anchors.left: parent.left
// anchors.right: parent.right
// height: 40
// color: palette.button
// radius: 6
// Rectangle {
// anchors.bottom: parent.bottom
// anchors.left: parent.left
// anchors.right: parent.right
// height: parent.radius
// color: parent.color
// }
// Text {
// anchors.centerIn: parent
// text: root.flowId
// color: palette.buttonText
// font.pixelSize: 14
// font.bold: true
// }
// }
// // // Tasks container
// // Row {
// // id: tasksRow
// // anchors.top: flowHeader.bottom
// // anchors.left: parent.left
// // anchors.margins: 25
// // anchors.topMargin: 25
// // objectName: "FlowTaskRow"
// // spacing: 40
// // Repeater {
// // model: root.taskModel
// // delegate: Task {
// // task: taskData
// // }
// // onItemAdded: function(index, item){
// // console.log("task added", index, item)
// // qtaskItems.push(item)
// // root.insertTaskItem(index, item)
// // }
// // onItemRemoved: function(index, item){
// // console.log("task added", index, item)
// // var idx = qtaskItems.indexOf(item)
// // if (idx !== -1) qtaskItems.splice(idx, 1)
// // }
// // }
// // }
// // Repeater {
// // model: root.connectionsModel
// // delegate: TaskConnection {
// // connection: connectionData
// // }
// // }
// }
// // Flow info tooltip
// Rectangle {
// id: infoTooltip
// anchors.top: parent.bottom
// anchors.left: parent.left
// anchors.topMargin: 5
// width: infoText.width + 20
// height: infoText.height + 10
// color: palette.base
// border.color: palette.shadow
// border.width: 1
// radius: 4
// visible: false
// Text {
// id: infoText
// anchors.centerIn: parent
// text: "Tasks: " + (root.taskModel ? root.taskModel.rowCount() : 0)
// color: palette.text
// font.pixelSize: 10
// }
// }
// MouseArea {
// anchors.fill: parent
// hoverEnabled: true
// onEntered: infoTooltip.visible = true
// onExited: infoTooltip.visible = false
// }
}

View File

@@ -1,140 +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/>.
*/
import QtQuick
import QtQuick.Controls
import TaskFlow.Editor
FlowEditor {
id: root
width: 1200
height: 800
property SystemPalette sysPalette: SystemPalette {
colorGroup: SystemPalette.Active
}
palette {
window: sysPalette.window
windowText: sysPalette.windowText
base: sysPalette.base
alternateBase: sysPalette.alternateBase
text: sysPalette.text
button: sysPalette.button
buttonText: sysPalette.buttonText
highlight: sysPalette.highlight
highlightedText: sysPalette.highlightedText
light: sysPalette.light
mid: sysPalette.mid
dark: sysPalette.dark
shadow: sysPalette.shadow
brightText: sysPalette.brightText
}
// Background with grid pattern
Rectangle {
anchors.fill: parent
color: palette.window
// Grid pattern using C++ implementation
GridBackground {
anchors.fill: parent
gridSize: 20
gridColor: palette.mid
opacity: 0.3
}
}
// Header panel
Rectangle {
id: headerPanel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 60
color: palette.base
border.color: palette.mid
border.width: 1
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 20
spacing: 20
Text {
text: "Flow Editor"
color: palette.windowText
font.pixelSize: 18
font.bold: true
}
Rectangle {
width: 2
height: 30
color: palette.mid
}
Text {
text: "Flow:"
color: palette.text
font.pixelSize: 14
}
ComboBox {
id: flowComboBox
model: root.flowsModel
textRole: "flowId"
currentIndex: root.currentFlowIndex
onActivated: {
root.currentFlowIndex = currentIndex
}
}
Text {
text: "Available Tasks: " + root.availableTaskTypes.join(", ")
color: palette.text
font.pixelSize: 12
}
}
}
// Main flow area
ScrollView {
id: scrollView
anchors.top: headerPanel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
contentWidth: flow.width
contentHeight: flow.height
Flow {
id: flow
// flow: root.currentFlow
width: Math.max(root.width, 0)
height: Math.min(root.height, 0)
}
}
}

View File

@@ -1,210 +0,0 @@
import QtQuick
import TaskFlow.Editor
TaskItem{
id: root
width: 280
height: Math.max(200, contentColumn.height + 40)
DragHandler {
id: dragHandler
target: root
onActiveChanged: {
if (active) {
root.z = 1000; // Поднять над остальными
} else {
root.z = 0;
}
}
}
// Task node background
Rectangle {
anchors.fill: parent
color: palette.window
border.color: palette.shadow
border.width: 1
radius: 6
// Task header
Rectangle {
id: taskHeader
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
height: 40
color: palette.button
radius: 6
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: parent.radius
color: parent.color
}
Text {
anchors.centerIn: parent
// text: root.taskType
color: palette.buttonText
font.pixelSize: 14
font.bold: true
}
}
// Task content
Column {
id: contentColumn
anchors.top: taskHeader.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 10
spacing: 8
// Task ID
Text {
text: "ID: " + root.taskId
color: palette.text
font.pixelSize: 11
width: parent.width
elide: Text.ElideRight
}
// Parameters section
Item {
width: parent.width
height: paramColumn.height
// visible: root.parameters && root.parameters.rowCount() > 0
Column {
id: paramColumn
width: parent.width
spacing: 6
Text {
text: "Parameters:"
color: palette.text
font.pixelSize: 10
font.bold: true
}
Repeater {
model: root.parameters
delegate: Rectangle {
width: parent.width
height: 24
color: palette.base
radius: 4
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
spacing: 6
Text {
text: paramKey + ":"
color: palette.text
font.pixelSize: 9
font.bold: true
}
Text {
text: paramValue
color: palette.windowText
font.pixelSize: 9
width: Math.min(150, implicitWidth)
elide: Text.ElideRight
}
}
}
}
}
}
}
}
// Input ports section (left side)
Column {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: -8
spacing: 6
// visible: root.inputPorts && root.inputPorts.rowCount() > 0
// Input label
Text {
text: "IN"
color: palette.highlight
font.pixelSize: 10
font.bold: true
anchors.left: parent.left
anchors.leftMargin: -20
}
// Repeater {
// model: root.inputPorts
// delegate: Row {
// spacing: 6
// Text {
// text: taskPortName
// color: palette.text
// font.pixelSize: 9
// anchors.verticalCenter: parent.verticalCenter
// horizontalAlignment: Text.AlignRight
// width: 60
// elide: Text.ElideLeft
// }
// TaskPort {
// port: taskPortData
// isInput: true
// }
// }
// }
}
// Output ports section (right side)
Column {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: -10
spacing: 8
// visible: root.outputPorts && root.outputPorts.rowCount() > 0
// Output label
Text {
text: "OUT"
color: palette.highlight
font.pixelSize: 10
font.bold: true
anchors.right: parent.right
anchors.rightMargin: -24
}
// Repeater {
// model: root.outputPorts
// delegate: Row {
// spacing: 6
// TaskPort {
// port: taskPortData
// isInput: false
// }
// Text {
// text: taskPortName
// color: palette.text
// font.pixelSize: 9
// anchors.verticalCenter: parent.verticalCenter
// width: 60
// elide: Text.ElideRight
// }
// }
// }
}
}

View File

@@ -1,68 +0,0 @@
import QtQuick
import QtQuick.Shapes
import TaskFlow.Editor
TaskConnectionItem {
id: root
property color connectionColor: "red"
Rectangle {
width: 10
height: 10
radius: width / 2
color: "blue"
}
// width: Math.abs(endPoint.x - startPoint.x) + 40
// height: Math.abs(endPoint.y - startPoint.y) + 40
// x: Math.min(startPoint.x, endPoint.x) - 20
// y: Math.min(startPoint.y, endPoint.y) - 20
// Shape {
// anchors.fill: parent
// ShapePath {
// strokeWidth: 2
// strokeColor: connectionColor
// fillColor: "transparent"
// property point localStart: Qt.point(
// root.startPoint.x - root.x,
// root.startPoint.y - root.y
// )
// property point localEnd: Qt.point(
// root.endPoint.x - root.x,
// root.endPoint.y - root.y
// )
// // Bezier curve
// property real controlOffset: Math.max(50, Math.abs(localEnd.x - localStart.x) * 0.4)
// startX: localStart.x
// startY: localStart.y
// PathCubic {
// x: parent.localEnd.x
// y: parent.localEnd.y
// control1X: parent.localStart.x + parent.controlOffset
// control1Y: parent.localStart.y
// control2X: parent.localEnd.x - parent.controlOffset
// control2Y: parent.localEnd.y
// }
// }
// // Arrow head
// Rectangle {
// width: 8
// height: 8
// color: connectionColor
// rotation: 45
// x: root.endPoint.x - root.x - 4
// y: root.endPoint.y - root.y - 4
// }
// }
// // Update positions when tasks might have moved
// Component.onCompleted: updatePositions()
}

View File

@@ -1,6 +0,0 @@
import QtQuick
import TaskFlow.Editor
Item {
}

View File

@@ -1,63 +0,0 @@
import QtQuick
import TaskFlow.Editor
TaskPortItem {
id: root
property bool isInput: true
width: 20
height: 20
// Port circle
Rectangle {
id: portCircle
anchors.centerIn: parent
width: 16
height: 16
radius: 8
color: getPortColor()
border.color: palette.windowText
border.width: 1
// Inner circle for connected state simulation
Rectangle {
anchors.centerIn: parent
width: 8
height: 8
radius: 4
color: root.port ? palette.windowText : "transparent"
visible: root.port !== null
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
portCircle.scale = 1.3
portCircle.border.width = 2
}
onExited: {
portCircle.scale = 1.0
portCircle.border.width = 1
}
}
function getPortColor() {
if (!root.port) return palette.mid
// Different colors for input/output using system palette
if (root.isInput) {
return palette.highlight // System highlight color for inputs
} else {
return Qt.lighter(palette.highlight, 1.3) // Lighter highlight for outputs
}
}
Behavior on scale {
NumberAnimation { duration: 100 }
}
}

View File

@@ -1,117 +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 "BaseTask.hpp"
#include "TaskPort.hpp"
#include <QUuid>
#include <QtConcurrent>
namespace QodeAssist::TaskFlow {
BaseTask::BaseTask(QObject *parent)
: QObject(parent)
, m_taskId("unknown" + QUuid::createUuid().toString())
{}
BaseTask::~BaseTask()
{
qDeleteAll(m_inputs);
qDeleteAll(m_outputs);
}
QString BaseTask::taskId() const
{
return m_taskId;
}
void BaseTask::setTaskId(const QString &taskId)
{
m_taskId = taskId;
}
QString BaseTask::taskType() const
{
return QString(metaObject()->className()).split("::").last();
}
void BaseTask::addInputPort(const QString &name)
{
QMutexLocker locker(&m_tasksMutex);
m_inputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
}
void BaseTask::addOutputPort(const QString &name)
{
QMutexLocker locker(&m_tasksMutex);
m_outputs.append(new TaskPort(name, TaskPort::ValueType::Any, this));
}
TaskPort *BaseTask::inputPort(const QString &name) const
{
QMutexLocker locker(&m_tasksMutex);
auto it = std::find_if(m_inputs.begin(), m_inputs.end(), [&name](const TaskPort *port) {
return port->name() == name;
});
return (it != m_inputs.end()) ? *it : nullptr;
}
TaskPort *BaseTask::outputPort(const QString &name) const
{
QMutexLocker locker(&m_tasksMutex);
auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&name](const TaskPort *port) {
return port->name() == name;
});
return (it != m_outputs.end()) ? *it : nullptr;
}
QList<TaskPort *> BaseTask::getInputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_inputs;
}
QList<TaskPort *> BaseTask::getOutputPorts() const
{
QMutexLocker locker(&m_tasksMutex);
return m_outputs;
}
QFuture<TaskState> BaseTask::executeAsync()
{
return QtConcurrent::task([this]() -> TaskState { return execute(); }).spawn();
}
QString BaseTask::taskStateAsString(TaskState state)
{
switch (state) {
case TaskState::Success:
return "Success";
case TaskState::Failed:
return "Failed";
case TaskState::Cancelled:
return "Cancelled";
}
return "Unknown";
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,68 +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 <QFuture>
#include <QMetaType>
#include <QMutex>
#include <QObject>
namespace QodeAssist::TaskFlow {
class TaskPort;
enum class TaskState { Success, Failed, Cancelled };
class BaseTask : public QObject
{
Q_OBJECT
public:
explicit BaseTask(QObject *parent = nullptr);
virtual ~BaseTask();
QString taskId() const;
void setTaskId(const QString &taskId);
QString taskType() const;
void addInputPort(const QString &name);
void addOutputPort(const QString &name);
TaskPort *inputPort(const QString &name) const;
TaskPort *outputPort(const QString &name) const;
QList<TaskPort *> getInputPorts() const;
QList<TaskPort *> getOutputPorts() const;
virtual TaskState execute() = 0;
static QString taskStateAsString(TaskState state);
protected:
QFuture<TaskState> executeAsync();
private:
QString m_taskId;
QList<TaskPort *> m_inputs;
QList<TaskPort *> m_outputs;
mutable QMutex m_tasksMutex;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,22 +0,0 @@
qt_add_library(TaskFlowCore STATIC
BaseTask.hpp BaseTask.cpp
TaskConnection.hpp TaskConnection.cpp
Flow.hpp Flow.cpp
TaskPort.hpp TaskPort.cpp
TaskRegistry.hpp TaskRegistry.cpp
FlowManager.hpp FlowManager.cpp
FlowRegistry.hpp FlowRegistry.cpp
)
target_link_libraries(TaskFlowCore
PUBLIC
Qt::Core
Qt::Concurrent
PRIVATE
QodeAssistLogger
)
target_include_directories(TaskFlowCore
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

View File

@@ -1,355 +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 "Flow.hpp"
#include "TaskPort.hpp"
#include <QUuid>
#include <QtConcurrent>
namespace QodeAssist::TaskFlow {
Flow::Flow(QObject *parent)
: QObject(parent)
, m_flowId("flow_" + QUuid::createUuid().toString())
{}
Flow::~Flow()
{
QMutexLocker locker(&m_flowMutex);
qDeleteAll(m_connections);
qDeleteAll(m_tasks);
}
QString Flow::flowId() const
{
return m_flowId;
}
void Flow::setFlowId(const QString &flowId)
{
if (m_flowId != flowId) {
m_flowId = flowId;
}
}
void Flow::addTask(BaseTask *task)
{
if (!task) {
return;
}
QMutexLocker locker(&m_flowMutex);
QString taskId = task->taskId();
if (m_tasks.contains(taskId)) {
qWarning() << "Flow::addTask - Task with ID" << taskId << "already exists";
return;
}
m_tasks.insert(taskId, task);
task->setParent(this);
emit taskAdded(taskId);
}
void Flow::removeTask(const QString &taskId)
{
QMutexLocker locker(&m_flowMutex);
BaseTask *task = m_tasks.value(taskId);
if (!task) {
return;
}
auto it = m_connections.begin();
while (it != m_connections.end()) {
TaskConnection *connection = *it;
if (connection->sourceTask() == task || connection->targetTask() == task) {
it = m_connections.erase(it);
emit connectionRemoved(connection);
delete connection;
} else {
++it;
}
}
m_tasks.remove(taskId);
emit taskRemoved(taskId);
delete task;
}
void Flow::removeTask(BaseTask *task)
{
if (!task) {
return;
}
removeTask(task->taskId());
}
BaseTask *Flow::getTask(const QString &taskId) const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.value(taskId);
}
bool Flow::hasTask(const QString &taskId) const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.contains(taskId);
}
QHash<QString, BaseTask *> Flow::tasks() const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks;
}
TaskConnection *Flow::addConnection(TaskPort *sourcePort, TaskPort *targetPort)
{
if (!sourcePort || !targetPort) {
qWarning() << "Flow::addConnection - Invalid ports";
return nullptr;
}
// Verify ports belong to tasks in this flow
BaseTask *sourceTask = qobject_cast<BaseTask *>(sourcePort->parent());
BaseTask *targetTask = qobject_cast<BaseTask *>(targetPort->parent());
if (!sourceTask || !targetTask) {
qWarning() << "Flow::addConnection - Ports don't belong to valid tasks";
return nullptr;
}
QMutexLocker locker(&m_flowMutex);
if (!m_tasks.contains(sourceTask->taskId()) || !m_tasks.contains(targetTask->taskId())) {
qWarning() << "Flow::addConnection - Tasks not in this flow";
return nullptr;
}
for (TaskConnection *existingConnection : m_connections) {
if (existingConnection->sourcePort() == sourcePort
&& existingConnection->targetPort() == targetPort) {
qWarning() << "Flow::addConnection - Connection already exists";
return existingConnection;
}
}
TaskConnection *connection = new TaskConnection(sourcePort, targetPort, this);
m_connections.append(connection);
emit connectionAdded(connection);
return connection;
}
void Flow::removeConnection(TaskConnection *connection)
{
if (!connection) {
return;
}
QMutexLocker locker(&m_flowMutex);
if (m_connections.removeOne(connection)) {
emit connectionRemoved(connection);
delete connection;
}
}
QList<TaskConnection *> Flow::connections() const
{
QMutexLocker locker(&m_flowMutex);
return m_connections;
}
QFuture<FlowState> Flow::executeAsync()
{
return QtConcurrent::run([this]() { return execute(); });
}
FlowState Flow::execute()
{
emit executionStarted();
if (!isValid()) {
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
if (hasCircularDependencies()) {
qWarning() << "Flow::execute - Circular dependencies detected";
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
QList<BaseTask *> executionOrder = getExecutionOrder();
for (BaseTask *task : executionOrder) {
TaskState taskResult = task->execute();
if (taskResult == TaskState::Failed) {
qWarning() << "Flow::execute - Task" << task->taskId() << "failed";
emit executionFinished(FlowState::Failed);
return FlowState::Failed;
}
if (taskResult == TaskState::Cancelled) {
qWarning() << "Flow::execute - Task" << task->taskId() << "cancelled";
emit executionFinished(FlowState::Cancelled);
return FlowState::Cancelled;
}
}
emit executionFinished(FlowState::Success);
return FlowState::Success;
}
bool Flow::isValid() const
{
QMutexLocker locker(&m_flowMutex);
// Check all connections are valid
for (TaskConnection *connection : m_connections) {
if (!connection->isValid()) {
return false;
}
}
return true;
}
bool Flow::hasCircularDependencies() const
{
return detectCircularDependencies();
}
QString Flow::flowStateAsString(FlowState state)
{
switch (state) {
case FlowState::Success:
return "Success";
case FlowState::Failed:
return "Failed";
case FlowState::Cancelled:
return "Cancelled";
}
return "Unknown";
}
QStringList Flow::getTaskIds() const
{
QMutexLocker locker(&m_flowMutex);
return m_tasks.keys();
}
QList<BaseTask *> Flow::getExecutionOrder() const
{
QMutexLocker locker(&m_flowMutex);
QList<BaseTask *> result;
QSet<BaseTask *> visited;
QList<BaseTask *> allTasks = m_tasks.values();
std::function<void(BaseTask *)> visit = [&](BaseTask *task) {
if (visited.contains(task)) {
return;
}
visited.insert(task);
QList<BaseTask *> dependencies = getTaskDependencies(task);
for (BaseTask *dependency : dependencies) {
visit(dependency);
}
result.append(task);
};
for (BaseTask *task : allTasks) {
visit(task);
}
return result;
}
bool Flow::detectCircularDependencies() const
{
QMutexLocker locker(&m_flowMutex);
QSet<BaseTask *> visited;
QSet<BaseTask *> recursionStack;
bool hasCycle = false;
for (BaseTask *task : m_tasks.values()) {
if (!visited.contains(task)) {
visitTask(task, visited, recursionStack, hasCycle);
if (hasCycle) {
return true;
}
}
}
return false;
}
void Flow::visitTask(
BaseTask *task, QSet<BaseTask *> &visited, QSet<BaseTask *> &recursionStack, bool &hasCycle) const
{
if (hasCycle) {
return;
}
visited.insert(task);
recursionStack.insert(task);
for (TaskConnection *connection : m_connections) {
if (connection->sourceTask() == task) {
BaseTask *dependentTask = connection->targetTask();
if (recursionStack.contains(dependentTask)) {
hasCycle = true;
return;
}
if (!visited.contains(dependentTask)) {
visitTask(dependentTask, visited, recursionStack, hasCycle);
}
}
}
recursionStack.remove(task);
}
QList<BaseTask *> Flow::getTaskDependencies(BaseTask *task) const
{
QList<BaseTask *> dependencies;
for (TaskConnection *connection : m_connections) {
if (connection->targetTask() == task) {
BaseTask *dependencyTask = connection->sourceTask();
if (!dependencies.contains(dependencyTask)) {
dependencies.append(dependencyTask);
}
}
}
return dependencies;
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,95 +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 <QFuture>
#include <QHash>
#include <QList>
#include <QMetaType>
#include <QMutex>
#include <QObject>
#include "BaseTask.hpp"
#include "TaskConnection.hpp"
namespace QodeAssist::TaskFlow {
enum class FlowState { Success, Failed, Cancelled };
class Flow : public QObject
{
Q_OBJECT
public:
explicit Flow(QObject *parent = nullptr);
~Flow() override;
QString flowId() const;
void setFlowId(const QString &flowId);
void addTask(BaseTask *task);
void removeTask(const QString &taskId);
void removeTask(BaseTask *task);
BaseTask *getTask(const QString &taskId) const;
bool hasTask(const QString &taskId) const;
QHash<QString, BaseTask *> tasks() const;
TaskConnection *addConnection(TaskPort *sourcePort, TaskPort *targetPort);
void removeConnection(TaskConnection *connection);
QList<TaskConnection *> connections() const;
QFuture<FlowState> executeAsync();
virtual FlowState execute();
bool isValid() const;
bool hasCircularDependencies() const;
static QString flowStateAsString(FlowState state);
QStringList getTaskIds() const;
signals:
void taskAdded(const QString &taskId);
void taskRemoved(const QString &taskId);
void connectionAdded(QodeAssist::TaskFlow::TaskConnection *connection);
void connectionRemoved(QodeAssist::TaskFlow::TaskConnection *connection);
void executionStarted();
void executionFinished(FlowState result);
private:
QString m_flowId;
QHash<QString, BaseTask *> m_tasks;
QList<TaskConnection *> m_connections;
mutable QMutex m_flowMutex;
QList<BaseTask *> getExecutionOrder() const;
bool detectCircularDependencies() const;
void visitTask(
BaseTask *task,
QSet<BaseTask *> &visited,
QSet<BaseTask *> &recursionStack,
bool &hasCycle) const;
QList<BaseTask *> getTaskDependencies(BaseTask *task) const;
};
} // namespace QodeAssist::TaskFlow
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::Flow *)
Q_DECLARE_METATYPE(QodeAssist::TaskFlow::FlowState)

View File

@@ -1,112 +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 "FlowManager.hpp"
#include <Logger.hpp>
#include <QFile>
#include <QFileInfo>
#include <QJsonDocument>
#include "FlowRegistry.hpp"
#include "TaskRegistry.hpp"
namespace QodeAssist::TaskFlow {
FlowManager::FlowManager(QObject *parent)
: QObject(parent)
, m_taskRegistry(new TaskRegistry(this))
, m_flowRegistry(new FlowRegistry(this))
{
LOG_MESSAGE("FlowManager created");
}
FlowManager::~FlowManager()
{
clear();
}
// Flow *FlowManager::createFlow(const QString &flowId)
// {
// Flow *flow = new Flow(flowId, m_taskRegistry, this);
// if (!m_flows.contains(flow->flowId())) {
// m_flows.insert(flowId, flow);
// } else {
// LOG_MESSAGE(
// QString("FlowManager::createFlow - flow with id %1 already exists").arg(flow->flowId()));
// }
// return flow;
// }
void FlowManager::addFlow(Flow *flow)
{
qDebug() << "FlowManager::addFlow" << flow->flowId();
if (!m_flows.contains(flow->flowId())) {
m_flows.insert(flow->flowId(), flow);
flow->setParent(this);
emit flowAdded(flow->flowId());
} else {
LOG_MESSAGE(
QString("FlowManager::addFlow - flow with id %1 already exists").arg(flow->flowId()));
}
}
void FlowManager::clear()
{
LOG_MESSAGE(QString("FlowManager::clear - removing %1 flows").arg(m_flows.size()));
qDeleteAll(m_flows);
m_flows.clear();
}
QStringList FlowManager::getAvailableTasksTypes()
{
return m_taskRegistry->getAvailableTypes();
}
QStringList FlowManager::getAvailableFlows()
{
return m_flowRegistry->getAvailableTypes();
}
QHash<QString, Flow *> FlowManager::flows() const
{
return m_flows;
}
TaskRegistry *FlowManager::taskRegistry() const
{
return m_taskRegistry;
}
FlowRegistry *FlowManager::flowRegistry() const
{
return m_flowRegistry;
}
Flow *FlowManager::getFlow(const QString &flowId) const
{
// if (flowId.isEmpty()) {
// return m_flows.begin().value();
// }
// return m_flows.value(flowId, nullptr);
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,69 +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 <QHash>
#include <QJsonArray>
#include <QJsonObject>
#include <QObject>
#include <QString>
#include "Flow.hpp"
namespace QodeAssist::TaskFlow {
class TaskRegistry;
class FlowRegistry;
class FlowManager : public QObject
{
Q_OBJECT
public:
explicit FlowManager(QObject *parent = nullptr);
~FlowManager() override;
// Flow *createFlow(const QString &flowId);
void addFlow(Flow *flow);
void clear();
QStringList getAvailableTasksTypes();
QStringList getAvailableFlows();
QHash<QString, Flow *> flows() const;
TaskRegistry *taskRegistry() const;
FlowRegistry *flowRegistry() const;
Flow *getFlow(const QString &flowId = {}) const;
signals:
void flowAdded(const QString &flowId);
void flowRemoved(const QString &flowId);
private:
QHash<QString, Flow *> m_flows;
TaskRegistry *m_taskRegistry;
FlowRegistry *m_flowRegistry;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,43 +0,0 @@
#include "FlowRegistry.hpp"
#include "Logger.hpp"
namespace QodeAssist::TaskFlow {
FlowRegistry::FlowRegistry(QObject *parent)
: QObject(parent)
{}
void FlowRegistry::registerFlow(const QString &flowType, FlowCreator creator)
{
m_flowCreators[flowType] = creator;
LOG_MESSAGE(QString("FlowRegistry: Registered flow type '%1'").arg(flowType));
}
Flow *FlowRegistry::createFlow(const QString &flowType, FlowManager *flowManager) const
{
LOG_MESSAGE(QString("Trying to create flow: %1").arg(flowType));
if (m_flowCreators.contains(flowType)) {
LOG_MESSAGE(QString("Found creator for flow type: %1").arg(flowType));
try {
Flow *flow = m_flowCreators[flowType](flowManager);
if (flow) {
LOG_MESSAGE(QString("Successfully created flow: %1").arg(flowType));
return flow;
}
} catch (...) {
LOG_MESSAGE(QString("Exception while creating flow of type: %1").arg(flowType));
}
} else {
LOG_MESSAGE(QString("No creator found for flow type: %1").arg(flowType));
}
return nullptr;
}
QStringList FlowRegistry::getAvailableTypes() const
{
return m_flowCreators.keys();
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,29 +0,0 @@
#pragma once
#include <functional>
#include <QHash>
#include <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class Flow;
class FlowManager;
class FlowRegistry : public QObject
{
Q_OBJECT
public:
using FlowCreator = std::function<Flow *(FlowManager *flowManager)>;
explicit FlowRegistry(QObject *parent = nullptr);
void registerFlow(const QString &flowType, FlowCreator creator);
Flow *createFlow(const QString &flowType, FlowManager *flowManager = nullptr) const;
QStringList getAvailableTypes() const;
private:
QHash<QString, FlowCreator> m_flowCreators;
};
} // namespace QodeAssist::TaskFlow

View File

@@ -1,125 +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 "TaskConnection.hpp"
#include "BaseTask.hpp"
#include "TaskPort.hpp"
#include <QMetaEnum>
namespace QodeAssist::TaskFlow {
TaskConnection::TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent)
: QObject(parent)
, m_sourcePort(sourcePort)
, m_targetPort(targetPort)
{
setupConnection();
}
TaskConnection::~TaskConnection()
{
cleanupConnection();
}
BaseTask *TaskConnection::sourceTask() const
{
return m_sourcePort ? qobject_cast<BaseTask *>(m_sourcePort->parent()) : nullptr;
}
BaseTask *TaskConnection::targetTask() const
{
return m_targetPort ? qobject_cast<BaseTask *>(m_targetPort->parent()) : nullptr;
}
TaskPort *TaskConnection::sourcePort() const
{
return m_sourcePort;
}
TaskPort *TaskConnection::targetPort() const
{
return m_targetPort;
}
bool TaskConnection::isValid() const
{
return m_sourcePort && m_targetPort && m_sourcePort != m_targetPort && sourceTask()
&& targetTask() && sourceTask() != targetTask();
}
bool TaskConnection::isTypeCompatible() const
{
if (!isValid()) {
return false;
}
return m_targetPort->isConnectionTypeCompatible(m_sourcePort);
}
QString TaskConnection::toString() const
{
if (!isValid()) {
return QString();
}
BaseTask *srcTask = sourceTask();
BaseTask *tgtTask = targetTask();
return QString("%1.%2->%3.%4")
.arg(srcTask->taskId())
.arg(m_sourcePort->name())
.arg(tgtTask->taskId())
.arg(m_targetPort->name());
}
bool TaskConnection::operator==(const TaskConnection &other) const
{
return m_sourcePort == other.m_sourcePort && m_targetPort == other.m_targetPort;
}
void TaskConnection::setupConnection()
{
if (!isValid()) {
qWarning() << "TaskConnection::setupConnection - Invalid connection parameters";
return;
}
if (!isTypeCompatible()) {
QMetaEnum metaEnum = QMetaEnum::fromType<TaskPort::ValueType>();
qWarning() << "TaskConnection::setupConnection - Type incompatible connection:"
<< metaEnum.valueToKey(static_cast<int>(m_sourcePort->valueType())) << "to"
<< metaEnum.valueToKey(static_cast<int>(m_targetPort->valueType()));
}
m_sourcePort->setConnection(this);
m_targetPort->setConnection(this);
}
void TaskConnection::cleanupConnection()
{
if (m_sourcePort && m_sourcePort->connection() == this) {
m_sourcePort->setConnection(nullptr);
}
if (m_targetPort && m_targetPort->connection() == this) {
m_targetPort->setConnection(nullptr);
}
}
} // namespace QodeAssist::TaskFlow

View File

@@ -1,65 +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 <QObject>
#include <QString>
namespace QodeAssist::TaskFlow {
class BaseTask;
class TaskPort;
class TaskConnection : public QObject
{
Q_OBJECT
public:
// Constructor automatically sets up the connection
explicit TaskConnection(TaskPort *sourcePort, TaskPort *targetPort, QObject *parent = nullptr);
// Destructor automatically cleans up the connection
~TaskConnection() override;
// Getters
BaseTask *sourceTask() const;
BaseTask *targetTask() const;
TaskPort *sourcePort() const;
TaskPort *targetPort() const;
// Validation
bool isValid() const;
bool isTypeCompatible() const;
// Utility
QString toString() const;
// Comparison
bool operator==(const TaskConnection &other) const;
private:
TaskPort *m_sourcePort;
TaskPort *m_targetPort;
void setupConnection();
void cleanupConnection();
};
} // namespace QodeAssist::TaskFlow

Some files were not shown because too many files have changed in this diff Show More