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
353 changed files with 5639 additions and 29839 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 polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev 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", "llm",
"ai" "ai"
], ],
"compatibility": "Qt 6.8.3", "compatibility": "Qt 6.8.1",
"platforms": [ "platforms": [
"Windows", "Windows",
"macOS", "macOS",
"Linux" "Linux"
], ],
"license": "GPLv3", "license": "GPLv3",
"version": "0.5.11", "version": "0.4.0",
"status": "draft", "status": "draft",
"is_pack": false, "is_pack": false,
"released_at": null, "released_at": null,
"version_history": [ "version_history": [
{ {
"version": "0.4.0", "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, "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", "icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",

View File

@@ -12,13 +12,16 @@ on:
env: env:
PLUGIN_NAME: QodeAssist 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" MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6" CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1" NINJA_VERSION: "1.12.1"
jobs: jobs:
build: 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 }} runs-on: ${{ matrix.config.os }}
outputs: outputs:
tag: ${{ steps.git.outputs.tag }} 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", environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
} }
- { - {
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64", name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-22.04, os: ubuntu-latest,
platform: linux_x64, platform: linux_x64,
cc: "gcc", cxx: "g++" cc: "gcc", cxx: "g++"
} }
@@ -44,22 +47,12 @@ jobs:
platform: mac_x64, platform: mac_x64,
cc: "clang", cxx: "clang++" cc: "clang", cxx: "clang++"
} }
qt_config:
- {
qt_version: "6.8.3",
qt_creator_version: "16.0.2"
}
- {
qt_version: "6.9.2",
qt_creator_version: "17.0.2"
}
- {
qt_version: "6.10.0",
qt_creator_version: "18.0.0"
}
steps: steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 - uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Checkout submodules - name: Checkout submodules
id: git id: git
@@ -68,21 +61,16 @@ jobs:
if (${{github.ref}} MATCHES "tags/v(.*)") if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}") file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
else() else()
execute_process( file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
COMMAND git rev-parse --short HEAD
OUTPUT_VARIABLE short_sha
OUTPUT_STRIP_TRAILING_WHITESPACE
)
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${short_sha}")
endif() endif()
- name: Download Ninja and CMake - name: Download Ninja and CMake
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541 uses: lukka/get-cmake@latest
with: with:
cmakeVersion: ${{ env.CMAKE_VERSION }} cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }} ninjaVersion: ${{ env.NINJA_VERSION }}
- name: Install dependencies - name: Install system libs
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
if ("${{ runner.os }}" STREQUAL "Linux") if ("${{ runner.os }}" STREQUAL "Linux")
@@ -90,13 +78,7 @@ jobs:
COMMAND sudo apt update COMMAND sudo apt update
) )
execute_process( execute_process(
COMMAND sudo apt install COMMAND sudo apt install libgl1-mesa-dev
# 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
RESULT_VARIABLE result RESULT_VARIABLE result
) )
if (NOT result EQUAL 0) if (NOT result EQUAL 0)
@@ -108,19 +90,14 @@ jobs:
id: qt id: qt
shell: cmake -P {0} shell: cmake -P {0}
run: | run: |
set(qt_version "${{ matrix.qt_config.qt_version }}") set(qt_version "$ENV{QT_VERSION}")
set(qt_creator_version "${{ matrix.qt_config.qt_creator_version }}")
string(REPLACE "." "" qt_version_dotless "${qt_version}") string(REPLACE "." "" qt_version_dotless "${qt_version}")
if ("${{ runner.os }}" STREQUAL "Windows") if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86") set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64") set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64") set(qt_dir_prefix "${qt_version}/msvc2022_64")
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else()
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64") set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
endif()
elseif ("${{ runner.os }}" STREQUAL "Linux") elseif ("${{ runner.os }}" STREQUAL "Linux")
set(url_os "linux_x64") set(url_os "linux_x64")
if (qt_version VERSION_LESS "6.7.0") if (qt_version VERSION_LESS "6.7.0")
@@ -129,20 +106,12 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64") set(qt_package_arch_suffix "linux_gcc_64")
endif() endif()
set(qt_dir_prefix "${qt_version}/gcc_64") set(qt_dir_prefix "${qt_version}/gcc_64")
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Linux-RHEL_9_4-GCC-Linux-RHEL_9_4-X86_64")
else()
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64") set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_64")
endif()
elseif ("${{ runner.os }}" STREQUAL "macOS") elseif ("${{ runner.os }}" STREQUAL "macOS")
set(url_os "mac_x64") set(url_os "mac_x64")
set(qt_package_arch_suffix "clang_64") set(qt_package_arch_suffix "clang_64")
set(qt_dir_prefix "${qt_version}/macos") 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") 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()
endif() endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}") set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
@@ -165,7 +134,7 @@ jobs:
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6) execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
endfunction() endfunction()
foreach(package qtbase qtdeclarative qttools) foreach(package qtbase qtdeclarative)
downloadAndExtract( downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z" "${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z ${package}.7z
@@ -199,11 +168,10 @@ jobs:
endif() endif()
- name: Download Qt Creator - name: Download Qt Creator
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac uses: qt-creator/install-dev-package@v1.2
with: with:
version: ${{ matrix.qt_config.qt_creator_version }} version: ${{ env.QT_CREATOR_VERSION }}
unzip-to: 'qtcreator' unzip-to: 'qtcreator'
platform: ${{ matrix.config.platform }}
- name: Extract Qt Creator - name: Extract Qt Creator
id: qt_creator id: qt_creator
@@ -249,7 +217,7 @@ jobs:
COMMAND python COMMAND python
-u -u
"${{ steps.qt_creator.outputs.qtc_dir }}/${build_plugin_py}" "${{ 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 . --src .
--build build --build build
--qt-path "${{ steps.qt.outputs.qt_dir }}" --qt-path "${{ steps.qt.outputs.qt_dir }}"
@@ -265,24 +233,67 @@ jobs:
endif() endif()
- name: Upload - name: Upload
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 uses: actions/upload-artifact@v4
with: with:
path: ./${{ 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}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z name: ${{ env.PLUGIN_NAME}}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
- name: Run unit tests # The json is the same for all platforms, but we need to save one
if: startsWith(matrix.config.os, 'ubuntu') - 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: | 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: release:
if: contains(github.ref, 'tags/v') if: contains(github.ref, 'tags/v')
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
needs: [build] needs: [build, update_json]
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 uses: actions/download-artifact@v4
with: with:
path: release-with-dirs path: release-with-dirs
@@ -291,21 +302,9 @@ jobs:
mkdir release mkdir release
mv release-with-dirs/*/* release/ mv release-with-dirs/*/* release/
- name: Download QodeAssistUpdater
run: |
# Get latest release info and download assets
LATEST_RELEASE=$(curl -s https://api.github.com/repos/Palm1r/QodeAssistUpdater/releases/latest)
# Download all assets except .sha256 files
echo "$LATEST_RELEASE" | jq -r '.assets[].browser_download_url' | grep -v '\.sha256$' | while read url; do
filename=$(basename "$url")
echo "Downloading $filename..."
curl -L -o "release/$filename" "$url"
done
- name: Create Release - name: Create Release
id: create_release id: create_release
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 uses: softprops/action-gh-release@v2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:

4
.gitignore vendored
View File

@@ -74,7 +74,3 @@ CMakeLists.txt.user*
*.exe *.exe
/build /build
/.qodeassist
/.cursor
/.vscode
.qtc_clangd/compile_commands.json

0
.gitmodules vendored
View File

View File

@@ -8,41 +8,15 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core) find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED) find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network 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}
)
add_subdirectory(llmcore) add_subdirectory(llmcore)
add_subdirectory(settings) add_subdirectory(settings)
add_subdirectory(logger) add_subdirectory(logger)
add_subdirectory(UIControls)
add_subdirectory(ChatView) add_subdirectory(ChatView)
add_subdirectory(context) add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS PLUGIN_DEPENDS
@@ -50,7 +24,6 @@ add_qtc_plugin(QodeAssist
QtCreator::LanguageClient QtCreator::LanguageClient
QtCreator::TextEditor QtCreator::TextEditor
QtCreator::ProjectExplorer QtCreator::ProjectExplorer
QtCreator::CppEditor
DEPENDS DEPENDS
Qt::Core Qt::Core
Qt::Gui Qt::Gui
@@ -59,7 +32,6 @@ add_qtc_plugin(QodeAssist
Qt::Network Qt::Network
QtCreator::ExtensionSystem QtCreator::ExtensionSystem
QtCreator::Utils QtCreator::Utils
QtCreator::CPlusPlus
QodeAssistChatViewplugin QodeAssistChatViewplugin
SOURCES SOURCES
.github/workflows/build_cmake.yml .github/workflows/build_cmake.yml
@@ -71,94 +43,33 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp templates/Templates.hpp
templates/CodeLlamaFim.hpp templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp templates/Qwen.hpp
templates/OpenAICompatible.hpp templates/Ollama.hpp
templates/BasicChat.hpp
templates/Llama3.hpp templates/Llama3.hpp
templates/ChatML.hpp templates/ChatML.hpp
templates/Alpaca.hpp templates/Alpaca.hpp
templates/Llama2.hpp templates/Llama2.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/CodeLlamaQMLFim.hpp templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
providers/Providers.hpp providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp 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/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
QodeAssist.qrc QodeAssist.qrc
LSPCompletion.hpp LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp LLMSuggestion.hpp LLMSuggestion.cpp
RefactorSuggestion.hpp RefactorSuggestion.cpp
RefactorSuggestionHoverHandler.hpp RefactorSuggestionHoverHandler.cpp
QodeAssistClient.hpp QodeAssistClient.cpp QodeAssistClient.hpp QodeAssistClient.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.cpp CodeHandler.hpp CodeHandler.cpp
UpdateStatusWidget.hpp UpdateStatusWidget.cpp UpdateStatusWidget.hpp UpdateStatusWidget.cpp
widgets/CompletionProgressHandler.hpp widgets/CompletionProgressHandler.cpp
widgets/CompletionErrorHandler.hpp widgets/CompletionErrorHandler.cpp
widgets/ProgressWidget.hpp widgets/ProgressWidget.cpp
widgets/ErrorWidget.hpp widgets/ErrorWidget.cpp
widgets/EditorChatButton.hpp widgets/EditorChatButton.cpp
widgets/EditorChatButtonHandler.hpp widgets/EditorChatButtonHandler.cpp
widgets/QuickRefactorDialog.hpp widgets/QuickRefactorDialog.cpp
widgets/CustomInstructionsManager.hpp widgets/CustomInstructionsManager.cpp
widgets/AddCustomInstructionDialog.hpp widgets/AddCustomInstructionDialog.cpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/EditFileTool.hpp tools/EditFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.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,23 +6,17 @@ qt_policy(SET QTP0004 NEW)
qt_add_qml_module(QodeAssistChatView qt_add_qml_module(QodeAssistChatView
URI ChatView URI ChatView
VERSION 1.0 VERSION 1.0
DEPENDENCIES DEPENDENCIES QtQuick
QtQuick
QML_FILES QML_FILES
qml/RootItem.qml qml/RootItem.qml
qml/ChatItem.qml qml/ChatItem.qml
qml/Badge.qml
qml/dialog/CodeBlock.qml qml/dialog/CodeBlock.qml
qml/dialog/TextBlock.qml qml/dialog/TextBlock.qml
qml/controls/QoAButton.qml
qml/parts/TopBar.qml qml/parts/TopBar.qml
qml/parts/BottomBar.qml qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml qml/parts/AttachedFilesPlace.qml
qml/parts/Toast.qml
qml/ToolStatusItem.qml
qml/ThinkingStatusItem.qml
qml/FileEditItem.qml
qml/parts/RulesViewer.qml
qml/parts/FileEditsActionBar.qml
RESOURCES RESOURCES
icons/attach-file-light.svg icons/attach-file-light.svg
icons/attach-file-dark.svg icons/attach-file-dark.svg
@@ -30,22 +24,6 @@ qt_add_qml_module(QodeAssistChatView
icons/close-light.svg icons/close-light.svg
icons/link-file-light.svg icons/link-file-light.svg
icons/link-file-dark.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
icons/rules-icon.svg
icons/open-in-editor.svg
icons/apply-changes-button.svg
icons/undo-changes-button.svg
icons/reject-changes-button.svg
icons/thinking-icon-on.svg
icons/thinking-icon-off.svg
SOURCES SOURCES
ChatWidget.hpp ChatWidget.cpp ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp ChatModel.hpp ChatModel.cpp
@@ -54,9 +32,6 @@ qt_add_qml_module(QodeAssistChatView
MessagePart.hpp MessagePart.hpp
ChatUtils.h ChatUtils.cpp ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp ChatSerializer.hpp ChatSerializer.cpp
ChatView.hpp ChatView.cpp
ChatData.hpp
) )
target_link_libraries(QodeAssistChatView target_link_libraries(QodeAssistChatView
@@ -70,8 +45,6 @@ target_link_libraries(QodeAssistChatView
LLMCore LLMCore
QodeAssistSettings QodeAssistSettings
Context Context
QodeAssistUIControlsplugin
QodeAssistLogger
) )
target_include_directories(QodeAssistChatView 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. * This file is part of QodeAssist.
* *
@@ -18,15 +18,11 @@
*/ */
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include <utils/aspects.h> #include <QtCore/qjsonobject.h>
#include <QDateTime>
#include <QJsonDocument>
#include <QJsonObject>
#include <QtQml> #include <QtQml>
#include <utils/aspects.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "Logger.hpp"
#include "context/ChangesManager.h"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -35,26 +31,10 @@ ChatModel::ChatModel(QObject *parent)
{ {
auto &settings = Settings::chatAssistantSettings(); auto &settings = Settings::chatAssistantSettings();
connect( connect(&settings.chatTokensThreshold,
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed, &Utils::BaseAspect::changed,
this, this,
&ChatModel::tokensThresholdChanged); &ChatModel::tokensThresholdChanged);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
&ChatModel::onFileEditApplied);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditRejected,
this,
&ChatModel::onFileEditRejected);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditArchived,
this,
&ChatModel::onFileEditArchived);
} }
int ChatModel::rowCount(const QModelIndex &parent) const int ChatModel::rowCount(const QModelIndex &parent) const
@@ -81,9 +61,6 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
} }
return filenames; return filenames;
} }
case Roles::IsRedacted: {
return message.isRedacted;
}
default: default:
return QVariant(); return QVariant();
} }
@@ -95,7 +72,6 @@ QHash<int, QByteArray> ChatModel::roleNames() const
roles[Roles::RoleType] = "roleType"; roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content"; roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments"; roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted";
return roles; return roles;
} }
@@ -114,8 +90,7 @@ void ChatModel::addMessage(
} }
} }
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
&& m_messages.last().role == role) {
Message &lastMessage = m_messages.last(); Message &lastMessage = m_messages.last();
lastMessage.content = content; lastMessage.content = content;
lastMessage.attachments = attachments; lastMessage.attachments = attachments;
@@ -126,45 +101,6 @@ void ChatModel::addMessage(
newMessage.attachments = attachments; newMessage.attachments = attachments;
m_messages.append(newMessage); m_messages.append(newMessage);
endInsertRows(); endInsertRows();
if (m_loadingFromHistory && role == ChatRole::FileEdit) {
const QString marker = "QODEASSIST_FILE_EDIT:";
if (content.contains(marker)) {
int markerPos = content.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < content.length()) {
QString jsonStr = content.mid(jsonStart);
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject editData = doc.object();
QString editId = editData.value("edit_id").toString();
QString filePath = editData.value("file").toString();
QString oldContent = editData.value("old_content").toString();
QString newContent = editData.value("new_content").toString();
QString originalStatus = editData.value("status").toString();
if (!editId.isEmpty() && !filePath.isEmpty()) {
Context::ChangesManager::instance().addFileEdit(
editId, filePath, oldContent, newContent, false, true);
editData["status"] = "archived";
editData["status_message"] = "Loaded from chat history";
QString updatedContent = marker
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
m_messages.last().content = updatedContent;
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
LOG_MESSAGE(QString("Registered historical file edit: %1 (original status: %2, now: archived)")
.arg(editId, originalStatus));
}
}
}
}
}
} }
} }
@@ -191,50 +127,19 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
while (blockMatches.hasNext()) { while (blockMatches.hasNext()) {
auto match = blockMatches.next(); auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) { if (match.capturedStart() > lastIndex) {
QString textBetween QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) { if (!textBetween.isEmpty()) {
MessagePart part; parts.append({MessagePart::Text, textBetween, ""});
part.type = MessagePartType::Text;
part.text = textBetween;
parts.append(part);
} }
} }
parts.append({MessagePart::Code, match.captured(2).trimmed(), match.captured(1)});
MessagePart codePart;
codePart.type = MessagePartType::Code;
codePart.text = match.captured(2).trimmed();
codePart.language = match.captured(1);
parts.append(codePart);
lastIndex = match.capturedEnd(); lastIndex = match.capturedEnd();
} }
if (lastIndex < content.length()) { if (lastIndex < content.length()) {
QString remainingText = content.mid(lastIndex).trimmed(); QString remainingText = content.mid(lastIndex).trimmed();
if (!remainingText.isEmpty()) {
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$"); parts.append({MessagePart::Text, remainingText, ""});
auto unclosedMatch = unclosedBlockRegex.match(remainingText);
if (unclosedMatch.hasMatch()) {
QString beforeCodeBlock = remainingText.left(unclosedMatch.capturedStart()).trimmed();
if (!beforeCodeBlock.isEmpty()) {
MessagePart part;
part.type = MessagePartType::Text;
part.text = beforeCodeBlock;
parts.append(part);
}
MessagePart codePart;
codePart.type = MessagePartType::Code;
codePart.text = unclosedMatch.captured(2).trimmed();
codePart.language = unclosedMatch.captured(1);
parts.append(codePart);
} else if (!remainingText.isEmpty()) {
MessagePart part;
part.type = MessagePartType::Text;
part.text = remainingText;
parts.append(part);
} }
} }
@@ -255,9 +160,6 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
case ChatRole::Assistant: case ChatRole::Assistant:
role = "assistant"; role = "assistant";
break; break;
case ChatRole::Tool:
case ChatRole::FileEdit:
continue;
default: default:
continue; continue;
} }
@@ -293,245 +195,4 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : ""; 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()));
bool toolMessageFound = false;
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));
toolMessageFound = true;
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
break;
}
}
if (!toolMessageFound) {
LOG_MESSAGE(QString("WARNING: Tool message with requestId=%1 toolId=%2 not found!")
.arg(requestId, toolId));
}
const QString marker = "QODEASSIST_FILE_EDIT:";
if (result.contains(marker)) {
LOG_MESSAGE(QString("File edit marker detected in tool result"));
int markerPos = result.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < result.length()) {
QString jsonStr = result.mid(jsonStart);
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
LOG_MESSAGE(QString("ERROR: Failed to parse file edit JSON at offset %1: %2")
.arg(parseError.offset)
.arg(parseError.errorString()));
} else if (!doc.isObject()) {
LOG_MESSAGE(
QString("ERROR: Parsed JSON is not an object, is array=%1").arg(doc.isArray()));
} else {
QJsonObject editData = doc.object();
QString editId = editData.value("edit_id").toString();
if (editId.isEmpty()) {
editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
}
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message fileEditMsg;
fileEditMsg.role = ChatRole::FileEdit;
fileEditMsg.content = result;
fileEditMsg.id = editId;
m_messages.append(fileEditMsg);
endInsertRows();
LOG_MESSAGE(QString("Added FileEdit message with editId=%1").arg(editId));
}
}
}
}
void ChatModel::addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature)
{
LOG_MESSAGE(QString("Adding thinking block: requestId=%1, thinking length=%2, signature length=%3")
.arg(requestId)
.arg(thinking.length())
.arg(signature.length()));
QString displayContent = thinking;
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = false;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::addRedactedThinkingBlock(const QString &requestId, const QString &signature)
{
LOG_MESSAGE(
QString("Adding redacted thinking block: requestId=%1, signature length=%2")
.arg(requestId)
.arg(signature.length()));
QString displayContent = "[Thinking content redacted by safety systems]";
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = true;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added redacted thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
{
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].id == messageId) {
m_messages[i].content = newContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
break;
}
}
}
void ChatModel::setLoadingFromHistory(bool loading)
{
m_loadingFromHistory = loading;
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
}
bool ChatModel::isLoadingFromHistory() const
{
return m_loadingFromHistory;
}
void ChatModel::onFileEditApplied(const QString &editId)
{
updateFileEditStatus(editId, "applied", "Successfully applied");
}
void ChatModel::onFileEditRejected(const QString &editId)
{
updateFileEditStatus(editId, "rejected", "Rejected by user");
}
void ChatModel::onFileEditArchived(const QString &editId)
{
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
}
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
{
const QString marker = "QODEASSIST_FILE_EDIT:";
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
const QString &content = m_messages[i].content;
if (content.contains(marker)) {
int markerPos = content.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < content.length()) {
QString jsonStr = content.mid(jsonStart);
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject editData = doc.object();
editData["status"] = status;
editData["status_message"] = statusMessage;
QString updatedContent = marker
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
m_messages[i].content = updatedContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
.arg(editId, status));
break;
}
}
}
}
}
}
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -37,19 +37,16 @@ class ChatModel : public QAbstractListModel
QML_ELEMENT QML_ELEMENT
public: public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking }; enum ChatRole { System, User, Assistant };
Q_ENUM(ChatRole) Q_ENUM(ChatRole)
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted }; enum Roles { RoleType = Qt::UserRole, Content, Attachments };
Q_ENUM(Roles)
struct Message struct Message
{ {
ChatRole role; ChatRole role;
QString content; QString content;
QString id; QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments; QList<Context::ContentFile> attachments;
}; };
@@ -76,37 +73,12 @@ public:
QString currentModel() const; QString currentModel() const;
QString lastMessageId() 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);
void addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature);
void addRedactedThinkingBlock(const QString &requestId, const QString &signature);
void updateMessageContent(const QString &messageId, const QString &newContent);
void setLoadingFromHistory(bool loading);
bool isLoadingFromHistory() const;
signals: signals:
void tokensThresholdChanged(); void tokensThresholdChanged();
void modelReseted(); void modelReseted();
private slots:
void onFileEditApplied(const QString &editId);
void onFileEditRejected(const QString &editId);
void onFileEditArchived(const QString &editId);
private: private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
QVector<Message> m_messages; QVector<Message> m_messages;
bool m_loadingFromHistory = false;
}; };
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -29,43 +29,40 @@
#include <projectexplorer/project.h> #include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h> #include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include <texteditor/texteditor.h> #include <projectexplorer/projecttree.h>
#include <utils/theme/theme.h> #include <utils/theme/theme.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp" #include "ChatSerializer.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "ProjectSettings.hpp" #include "ProjectSettings.hpp"
#include "context/ChangesManager.h"
#include "context/ContextManager.hpp" #include "context/ContextManager.hpp"
#include "context/FileChunker.hpp"
#include "context/RAGManager.hpp"
#include "context/TokenUtils.hpp" #include "context/TokenUtils.hpp"
#include "llmcore/RulesLoader.hpp"
#include "ProvidersManager.hpp"
#include "GeneralSettings.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ChatRootView::ChatRootView(QQuickItem *parent) ChatRootView::ChatRootView(QQuickItem *parent)
: QQuickItem(parent) : QQuickItem(parent)
, m_chatModel(new ChatModel(this)) , m_chatModel(new ChatModel(this))
, m_promptProvider(LLMCore::PromptTemplateManager::instance()) , m_clientInterface(new ClientInterface(m_chatModel, this))
, m_clientInterface(new ClientInterface(m_chatModel, &m_promptProvider, this))
, m_isRequestInProgress(false)
{ {
m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles(); m_isSyncOpenFiles = Settings::chatAssistantSettings().linkOpenFiles();
connect( connect(&Settings::chatAssistantSettings().linkOpenFiles, &Utils::BaseAspect::changed,
&Settings::chatAssistantSettings().linkOpenFiles,
&Utils::BaseAspect::changed,
this, this,
[this]() { setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles()); }); [this](){
setIsSyncOpenFiles(Settings::chatAssistantSettings().linkOpenFiles());
});
auto &settings = Settings::generalSettings(); auto &settings = Settings::generalSettings();
connect( connect(&settings.caModel,
&settings.caModel, &Utils::BaseAspect::changed, this, &ChatRootView::currentTemplateChanged); &Utils::BaseAspect::changed,
this,
&ChatRootView::currentTemplateChanged);
connect( connect(
m_clientInterface, m_clientInterface,
@@ -73,33 +70,19 @@ ChatRootView::ChatRootView(QQuickItem *parent)
this, this,
&ChatRootView::autosave); &ChatRootView::autosave);
connect(m_clientInterface, &ClientInterface::messageReceivedCompletely, this, [this]() {
this->setRequestProgressStatus(false);
});
connect( connect(
m_clientInterface, m_clientInterface,
&ClientInterface::messageReceivedCompletely, &ClientInterface::messageReceivedCompletely,
this, this,
&ChatRootView::updateInputTokensCount); &ChatRootView::updateInputTokensCount);
connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { connect(m_chatModel, &ChatModel::modelReseted, this, [this]() { setRecentFilePath(QString{}); });
setRecentFilePath(QString{});
m_currentMessageRequestId.clear();
updateCurrentMessageEditsStats();
});
connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount); connect(this, &ChatRootView::attachmentFilesChanged, &ChatRootView::updateInputTokensCount);
connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount); connect(this, &ChatRootView::linkedFilesChanged, &ChatRootView::updateInputTokensCount);
connect( connect(&Settings::chatAssistantSettings().useSystemPrompt, &Utils::BaseAspect::changed,
&Settings::chatAssistantSettings().useSystemPrompt, this, &ChatRootView::updateInputTokensCount);
&Utils::BaseAspect::changed, connect(&Settings::chatAssistantSettings().systemPrompt, &Utils::BaseAspect::changed,
this, this, &ChatRootView::updateInputTokensCount);
&ChatRootView::updateInputTokensCount);
connect(
&Settings::chatAssistantSettings().systemPrompt,
&Utils::BaseAspect::changed,
this,
&ChatRootView::updateInputTokensCount);
auto editors = Core::EditorManager::instance(); auto editors = Core::EditorManager::instance();
@@ -117,106 +100,8 @@ 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();
});
connect(m_clientInterface, &ClientInterface::requestStarted, this, [this](const QString &requestId) {
if (!m_currentMessageRequestId.isEmpty()) {
LOG_MESSAGE(QString("Clearing previous message requestId: %1").arg(m_currentMessageRequestId));
}
m_currentMessageRequestId = requestId;
LOG_MESSAGE(QString("New message request started: %1").arg(requestId));
updateCurrentMessageEditsStats();
});
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditAdded,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditRejected,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditUndone,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
connect(
&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditArchived,
this,
[this](const QString &) { updateCurrentMessageEditsStats(); });
updateInputTokensCount(); updateInputTokensCount();
refreshRules();
connect(
ProjectExplorer::ProjectManager::instance(),
&ProjectExplorer::ProjectManager::startupProjectChanged,
this,
&ChatRootView::refreshRules);
QSettings appSettings;
m_isAgentMode = appSettings.value("QodeAssist/Chat/AgentMode", false).toBool();
m_isThinkingMode = Settings::chatAssistantSettings().enableThinkingMode();
connect(
&Settings::chatAssistantSettings().enableThinkingMode,
&Utils::BaseAspect::changed,
this,
[this]() { setIsThinkingMode(Settings::chatAssistantSettings().enableThinkingMode()); });
connect(
&Settings::toolsSettings().useTools,
&Utils::BaseAspect::changed,
this,
&ChatRootView::toolsSupportEnabledChanged);
connect(
&Settings::generalSettings().caProvider,
&Utils::BaseAspect::changed,
this,
&ChatRootView::isThinkingSupportChanged);
} }
ChatModel *ChatRootView::chatModel() const ChatModel *ChatRootView::chatModel() const
@@ -242,9 +127,8 @@ void ChatRootView::sendMessage(const QString &message)
} }
} }
m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles, m_isAgentMode); m_clientInterface->sendMessage(message, m_attachmentFiles, m_linkedFiles);
clearAttachmentFiles(); clearAttachmentFiles();
setRequestProgressStatus(true);
} }
void ChatRootView::copyToClipboard(const QString &text) void ChatRootView::copyToClipboard(const QString &text)
@@ -255,7 +139,6 @@ void ChatRootView::copyToClipboard(const QString &text)
void ChatRootView::cancelRequest() void ChatRootView::cancelRequest()
{ {
m_clientInterface->cancelRequest(); m_clientInterface->cancelRequest();
setRequestProgressStatus(false);
} }
void ChatRootView::clearAttachmentFiles() void ChatRootView::clearAttachmentFiles()
@@ -280,10 +163,9 @@ QString ChatRootView::getChatsHistoryDir() const
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString(); path = projectSettings.chatHistoryPath().toString();
} else { } else {
path = QString("%1/qodeassist/chat_history") path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
.arg(Core::ICore::userResourcePath().toFSPathString());
} }
QDir dir(path); QDir dir(path);
@@ -319,10 +201,7 @@ void ChatRootView::loadHistory(const QString &filePath)
} else { } else {
setRecentFilePath(filePath); setRecentFilePath(filePath);
} }
m_currentMessageRequestId.clear();
updateInputTokensCount(); updateInputTokensCount();
updateCurrentMessageEditsStats();
} }
void ChatRootView::showSaveDialog() void ChatRootView::showSaveDialog()
@@ -420,7 +299,8 @@ QString ChatRootView::getSuggestedFileName() const
QFileInfo finalCheck(fullPath); QFileInfo finalCheck(fullPath);
if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) { if (fileName.isEmpty() || finalCheck.exists() || !QFileInfo(finalCheck.path()).isWritable()) {
fileName = QString("chat_%1").arg(QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm")); fileName = QString("chat_%1").arg(
QDateTime::currentDateTime().toString("yyyy-MM-dd_HH-mm"));
} }
return fileName; return fileName;
@@ -469,7 +349,7 @@ void ChatRootView::showAttachFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString()); dialog.setDirectory(project->projectDirectory().toString());
} }
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
@@ -503,7 +383,7 @@ void ChatRootView::showLinkFilesDialog()
dialog.setFileMode(QFileDialog::ExistingFiles); dialog.setFileMode(QFileDialog::ExistingFiles);
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
dialog.setDirectory(project->projectDirectory().toFSPathString()); dialog.setDirectory(project->projectDirectory().toString());
} }
if (dialog.exec() == QDialog::Accepted) { if (dialog.exec() == QDialog::Accepted) {
@@ -556,10 +436,9 @@ void ChatRootView::openChatHistoryFolder()
QString path; QString path;
if (auto project = ProjectExplorer::ProjectManager::startupProject()) { if (auto project = ProjectExplorer::ProjectManager::startupProject()) {
Settings::ProjectSettings projectSettings(project); Settings::ProjectSettings projectSettings(project);
path = projectSettings.chatHistoryPath().toFSPathString(); path = projectSettings.chatHistoryPath().toString();
} else { } else {
path = QString("%1/qodeassist/chat_history") path = QString("%1/qodeassist/chat_history").arg(Core::ICore::userResourcePath().toString());
.arg(Core::ICore::userResourcePath().toFSPathString());
} }
QDir dir(path); QDir dir(path);
@@ -571,46 +450,90 @@ void ChatRootView::openChatHistoryFolder()
QDesktopServices::openUrl(url); QDesktopServices::openUrl(url);
} }
void ChatRootView::openRulesFolder() // ChatRootView.cpp
void ChatRootView::testRAG(const QString &message)
{ {
auto project = ProjectExplorer::ProjectManager::startupProject(); auto project = ProjectExplorer::ProjectTree::currentProject();
if (!project) { if (!project) {
qDebug() << "No active project found";
return; return;
} }
QString projectPath = project->projectDirectory().toFSPathString(); const QString TEST_QUERY = message;
QString rulesPath = projectPath + "/.qodeassist/rules";
QDir dir(rulesPath); qDebug() << "Starting RAG test with query:";
if (!dir.exists()) { qDebug() << TEST_QUERY;
dir.mkpath("."); 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;
} }
QUrl url = QUrl::fromLocalFile(dir.absolutePath()); Context::FileChunker::ChunkingConfig config;
QDesktopServices::openUrl(url); Context::ContextManager::instance().testProjectChunks(project, config);
} }
void ChatRootView::updateInputTokensCount() void ChatRootView::updateInputTokensCount()
{ {
int inputTokens = m_messageTokensCount; int inputTokens = m_messageTokensCount;
auto &settings = Settings::chatAssistantSettings(); auto& settings = Settings::chatAssistantSettings();
if (settings.useSystemPrompt()) { if (settings.useSystemPrompt()) {
inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt()); inputTokens += Context::TokenUtils::estimateTokens(settings.systemPrompt());
} }
if (!m_attachmentFiles.isEmpty()) { 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); inputTokens += Context::TokenUtils::estimateFilesTokens(attachFiles);
} }
if (!m_linkedFiles.isEmpty()) { 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); inputTokens += Context::TokenUtils::estimateFilesTokens(linkFiles);
} }
const auto &history = m_chatModel->getChatHistory(); const auto& history = m_chatModel->getChatHistory();
for (const auto &message : history) { for (const auto& message : history) {
inputTokens += Context::TokenUtils::estimateTokens(message.content); inputTokens += Context::TokenUtils::estimateTokens(message.content);
inputTokens += 4; // + role inputTokens += 4; // + role
} }
@@ -632,7 +555,7 @@ bool ChatRootView::isSyncOpenFiles() const
void ChatRootView::onEditorAboutToClose(Core::IEditor *editor) void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
{ {
if (auto document = editor->document(); document && isSyncOpenFiles()) { if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString(); QString filePath = document->filePath().toString();
m_linkedFiles.removeOne(filePath); m_linkedFiles.removeOne(filePath);
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
@@ -645,8 +568,8 @@ void ChatRootView::onEditorAboutToClose(Core::IEditor *editor)
void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor) void ChatRootView::onAppendLinkFileFromEditor(Core::IEditor *editor)
{ {
if (auto document = editor->document(); document && isSyncOpenFiles()) { if (auto document = editor->document(); document && isSyncOpenFiles()) {
QString filePath = document->filePath().toFSPathString(); QString filePath = document->filePath().toString();
if (!m_linkedFiles.contains(filePath) && !shouldIgnoreFileForAttach(document->filePath())) { if (!m_linkedFiles.contains(filePath)) {
m_linkedFiles.append(filePath); m_linkedFiles.append(filePath);
emit linkedFilesChanged(); emit linkedFilesChanged();
} }
@@ -673,458 +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;
}
QVariantList ChatRootView::activeRules() const
{
return m_activeRules;
}
int ChatRootView::activeRulesCount() const
{
return m_activeRules.size();
}
QString ChatRootView::getRuleContent(int index)
{
if (index < 0 || index >= m_activeRules.size())
return QString();
return LLMCore::RulesLoader::loadRuleFileContent(
m_activeRules[index].toMap()["filePath"].toString());
}
void ChatRootView::refreshRules()
{
m_activeRules.clear();
auto project = LLMCore::RulesLoader::getActiveProject();
if (!project) {
emit activeRulesChanged();
emit activeRulesCountChanged();
return;
}
auto ruleFiles
= LLMCore::RulesLoader::getRuleFilesForProject(project, LLMCore::RulesContext::Chat);
for (const auto &ruleFile : ruleFiles) {
QVariantMap ruleMap;
ruleMap["filePath"] = ruleFile.filePath;
ruleMap["fileName"] = ruleFile.fileName;
ruleMap["category"] = ruleFile.category;
m_activeRules.append(ruleMap);
}
emit activeRulesChanged();
emit activeRulesCountChanged();
}
bool ChatRootView::isAgentMode() const
{
return m_isAgentMode;
}
void ChatRootView::setIsAgentMode(bool newIsAgentMode)
{
if (m_isAgentMode != newIsAgentMode) {
m_isAgentMode = newIsAgentMode;
QSettings settings;
settings.setValue("QodeAssist/Chat/AgentMode", newIsAgentMode);
emit isAgentModeChanged();
}
}
bool ChatRootView::isThinkingMode() const
{
return m_isThinkingMode;
}
void ChatRootView::setIsThinkingMode(bool newIsThinkingMode)
{
if (m_isThinkingMode != newIsThinkingMode) {
m_isThinkingMode = newIsThinkingMode;
Settings::chatAssistantSettings().enableThinkingMode.setValue(newIsThinkingMode);
Settings::chatAssistantSettings().writeSettings();
emit isThinkingModeChanged();
}
}
bool ChatRootView::toolsSupportEnabled() const
{
return Settings::toolsSettings().useTools();
}
void ChatRootView::applyFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Applying file edit: %1").arg(editId));
if (Context::ChangesManager::instance().applyFileEdit(editId)) {
m_lastInfoMessage = QString("File edit applied successfully");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "applied");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to apply file edit")
: QString("Failed to apply file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::rejectFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Rejecting file edit: %1").arg(editId));
if (Context::ChangesManager::instance().rejectFileEdit(editId)) {
m_lastInfoMessage = QString("File edit rejected");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to reject file edit")
: QString("Failed to reject file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::undoFileEdit(const QString &editId)
{
LOG_MESSAGE(QString("Undoing file edit: %1").arg(editId));
if (Context::ChangesManager::instance().undoFileEdit(editId)) {
m_lastInfoMessage = QString("File edit undone successfully");
emit lastInfoMessageChanged();
updateFileEditStatus(editId, "rejected");
} else {
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
m_lastErrorMessage = edit.statusMessage.isEmpty()
? QString("Failed to undo file edit")
: QString("Failed to undo file edit: %1").arg(edit.statusMessage);
emit lastErrorMessageChanged();
}
}
void ChatRootView::openFileEditInEditor(const QString &editId)
{
LOG_MESSAGE(QString("Opening file edit in editor: %1").arg(editId));
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (edit.editId.isEmpty()) {
m_lastErrorMessage = QString("File edit not found: %1").arg(editId);
emit lastErrorMessageChanged();
return;
}
Utils::FilePath filePath = Utils::FilePath::fromString(edit.filePath);
Core::IEditor *editor = Core::EditorManager::openEditor(filePath);
if (!editor) {
m_lastErrorMessage = QString("Failed to open file in editor: %1").arg(edit.filePath);
emit lastErrorMessageChanged();
return;
}
auto *textEditor = qobject_cast<TextEditor::BaseTextEditor *>(editor);
if (textEditor && textEditor->editorWidget()) {
QTextDocument *doc = textEditor->editorWidget()->document();
if (doc) {
QString currentContent = doc->toPlainText();
int position = -1;
if (edit.status == Context::ChangesManager::Applied && !edit.newContent.isEmpty()) {
position = currentContent.indexOf(edit.newContent);
}
else if (!edit.oldContent.isEmpty()) {
position = currentContent.indexOf(edit.oldContent);
}
if (position >= 0) {
QTextCursor cursor(doc);
cursor.setPosition(position);
textEditor->editorWidget()->setTextCursor(cursor);
textEditor->editorWidget()->centerCursor();
}
}
}
LOG_MESSAGE(QString("Opened file in editor: %1").arg(edit.filePath));
}
void ChatRootView::updateFileEditStatus(const QString &editId, const QString &status)
{
auto messages = m_chatModel->getChatHistory();
for (int i = 0; i < messages.size(); ++i) {
if (messages[i].role == Chat::ChatModel::FileEdit && messages[i].id == editId) {
QString content = messages[i].content;
const QString marker = "QODEASSIST_FILE_EDIT:";
int markerPos = content.indexOf(marker);
QString jsonStr = content;
if (markerPos >= 0) {
jsonStr = content.mid(markerPos + marker.length());
}
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject obj = doc.object();
obj["status"] = status;
auto edit = Context::ChangesManager::instance().getFileEdit(editId);
if (!edit.statusMessage.isEmpty()) {
obj["status_message"] = edit.statusMessage;
}
QString updatedContent = marker + QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact));
m_chatModel->updateMessageContent(editId, updatedContent);
LOG_MESSAGE(QString("Updated file edit status to: %1").arg(status));
}
break;
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::applyAllFileEditsForCurrentMessage()
{
if (m_currentMessageRequestId.isEmpty()) {
m_lastErrorMessage = QString("No active message with file edits");
emit lastErrorMessageChanged();
return;
}
LOG_MESSAGE(QString("Applying all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.reapplyAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) {
m_lastInfoMessage = QString("All file edits applied successfully");
emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
} else {
m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to apply some file edits")
: QString("Failed to apply some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Applied) {
updateFileEditStatus(edit.editId, "applied");
}
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::undoAllFileEditsForCurrentMessage()
{
if (m_currentMessageRequestId.isEmpty()) {
m_lastErrorMessage = QString("No active message with file edits");
emit lastErrorMessageChanged();
return;
}
LOG_MESSAGE(QString("Undoing all file edits for message: %1").arg(m_currentMessageRequestId));
QString errorMsg;
bool success = Context::ChangesManager::instance()
.undoAllEditsForRequest(m_currentMessageRequestId, &errorMsg);
if (success) {
m_lastInfoMessage = QString("All file edits undone successfully");
emit lastInfoMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
} else {
m_lastErrorMessage = errorMsg.isEmpty()
? QString("Failed to undo some file edits")
: QString("Failed to undo some file edits:\n%1").arg(errorMsg);
emit lastErrorMessageChanged();
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
for (const auto &edit : edits) {
if (edit.status == Context::ChangesManager::Rejected) {
updateFileEditStatus(edit.editId, "rejected");
}
}
}
updateCurrentMessageEditsStats();
}
void ChatRootView::updateCurrentMessageEditsStats()
{
if (m_currentMessageRequestId.isEmpty()) {
if (m_currentMessageTotalEdits != 0 || m_currentMessageAppliedEdits != 0 ||
m_currentMessagePendingEdits != 0 || m_currentMessageRejectedEdits != 0) {
m_currentMessageTotalEdits = 0;
m_currentMessageAppliedEdits = 0;
m_currentMessagePendingEdits = 0;
m_currentMessageRejectedEdits = 0;
emit currentMessageEditsStatsChanged();
}
return;
}
auto edits = Context::ChangesManager::instance().getEditsForRequest(m_currentMessageRequestId);
int total = edits.size();
int applied = 0;
int pending = 0;
int rejected = 0;
for (const auto &edit : edits) {
switch (edit.status) {
case Context::ChangesManager::Applied:
applied++;
break;
case Context::ChangesManager::Pending:
pending++;
break;
case Context::ChangesManager::Rejected:
rejected++;
break;
case Context::ChangesManager::Archived:
total--;
break;
}
}
bool changed = false;
if (m_currentMessageTotalEdits != total) {
m_currentMessageTotalEdits = total;
changed = true;
}
if (m_currentMessageAppliedEdits != applied) {
m_currentMessageAppliedEdits = applied;
changed = true;
}
if (m_currentMessagePendingEdits != pending) {
m_currentMessagePendingEdits = pending;
changed = true;
}
if (m_currentMessageRejectedEdits != rejected) {
m_currentMessageRejectedEdits = rejected;
changed = true;
}
if (changed) {
LOG_MESSAGE(QString("Updated message edits stats: total=%1, applied=%2, pending=%3, rejected=%4")
.arg(total).arg(applied).arg(pending).arg(rejected));
emit currentMessageEditsStatsChanged();
}
}
int ChatRootView::currentMessageTotalEdits() const
{
return m_currentMessageTotalEdits;
}
int ChatRootView::currentMessageAppliedEdits() const
{
return m_currentMessageAppliedEdits;
}
int ChatRootView::currentMessagePendingEdits() const
{
return m_currentMessagePendingEdits;
}
int ChatRootView::currentMessageRejectedEdits() const
{
return m_currentMessageRejectedEdits;
}
QString ChatRootView::lastInfoMessage() const
{
return m_lastInfoMessage;
}
bool ChatRootView::isThinkingSupport() const
{
auto providerName = Settings::generalSettings().caProvider();
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
return provider && provider->supportThinking();
}
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -23,7 +23,6 @@
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -38,26 +37,6 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL) Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL) Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged 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)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
Q_PROPERTY(
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
QML_ELEMENT QML_ELEMENT
@@ -86,7 +65,8 @@ public:
Q_INVOKABLE void calculateMessageTokensCount(const QString &message); Q_INVOKABLE void calculateMessageTokensCount(const QString &message);
Q_INVOKABLE void setIsSyncOpenFiles(bool state); Q_INVOKABLE void setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder(); Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder(); Q_INVOKABLE void testRAG(const QString &message);
Q_INVOKABLE void testChunking();
Q_INVOKABLE void updateInputTokensCount(); Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const; int inputTokensCount() const;
@@ -99,48 +79,6 @@ public:
QString chatFileName() const; QString chatFileName() const;
void setRecentFilePath(const QString &filePath); 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;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
bool isAgentMode() const;
void setIsAgentMode(bool newIsAgentMode);
bool isThinkingMode() const;
void setIsThinkingMode(bool newIsThinkingMode);
bool toolsSupportEnabled() const;
Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId);
Q_INVOKABLE void undoFileEdit(const QString &editId);
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats();
int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const;
int currentMessageRejectedEdits() const;
QString lastInfoMessage() const;
bool isThinkingSupport() const;
public slots: public slots:
void sendMessage(const QString &message); void sendMessage(const QString &message);
@@ -157,33 +95,12 @@ signals:
void inputTokensCountChanged(); void inputTokensCountChanged();
void isSyncOpenFilesChanged(); void isSyncOpenFilesChanged();
void chatFileNameChanged(); void chatFileNameChanged();
void textFamilyChanged();
void codeFamilyChanged();
void codeFontSizeChanged();
void textFontSizeChanged();
void textFormatChanged();
void chatRequestStarted();
void isRequestInProgressChanged();
void lastErrorMessageChanged();
void lastInfoMessageChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void isAgentModeChanged();
void isThinkingModeChanged();
void toolsSupportEnabledChanged();
void currentMessageEditsStatsChanged();
void isThinkingSupportChanged();
private: private:
void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const; QString getChatsHistoryDir() const;
QString getSuggestedFileName() const; QString getSuggestedFileName() const;
ChatModel *m_chatModel; ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface; ClientInterface *m_clientInterface;
QString m_currentTemplate; QString m_currentTemplate;
QString m_recentFilePath; QString m_recentFilePath;
@@ -193,18 +110,6 @@ private:
int m_inputTokensCount{0}; int m_inputTokensCount{0};
bool m_isSyncOpenFiles; bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors; QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
bool m_isAgentMode;
bool m_isThinkingMode;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage;
}; };
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -83,10 +83,6 @@ QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
messageObj["role"] = static_cast<int>(message.role); messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content; messageObj["content"] = message.content;
messageObj["id"] = message.id; messageObj["id"] = message.id;
messageObj["isRedacted"] = message.isRedacted;
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
return messageObj; return messageObj;
} }
@@ -96,8 +92,6 @@ ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt()); message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString(); message.content = json["content"].toString();
message.id = json["id"].toString(); message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
return message; return message;
} }
@@ -126,15 +120,10 @@ bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
} }
model->clear(); model->clear();
model->setLoadingFromHistory(true);
for (const auto &message : messages) { for (const auto &message : messages) {
model->addMessage(message.content, message.role, message.id); model->addMessage(message.content, message.role, message.id);
} }
model->setLoadingFromHistory(false);
return true; return true;
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * 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. * This file is part of QodeAssist.
* *
@@ -29,40 +29,4 @@ void ChatUtils::copyToClipboard(const QString &text)
QGuiApplication::clipboard()->setText(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 } // 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. * This file is part of QodeAssist.
* *
@@ -34,7 +34,6 @@ public:
: QObject(parent) {}; : QObject(parent) {};
Q_INVOKABLE void copyToClipboard(const QString &text); Q_INVOKABLE void copyToClipboard(const QString &text);
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
}; };
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -40,4 +40,4 @@ void ChatWidget::scrollToBottom()
{ {
QMetaObject::invokeMethod(rootObject(), "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. * This file is part of QodeAssist.
* *
@@ -38,4 +38,4 @@ signals:
void clearPressed(); 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. * This file is part of QodeAssist.
* *
@@ -19,58 +19,58 @@
#include "ClientInterface.hpp" #include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFileInfo> #include <QFileInfo>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QUuid> #include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h> #include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h> #include <coreplugin/idocument.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h> #include <texteditor/textdocument.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include "ChatAssistantSettings.hpp" #include "ChatAssistantSettings.hpp"
#include "ContextManager.hpp"
#include "GeneralSettings.hpp" #include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp" #include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp" #include "ProvidersManager.hpp"
#include "RequestConfig.hpp"
#include <context/ChangesManager.h>
#include <RulesLoader.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
ClientInterface::ClientInterface( ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent) : QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel) , m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{}
ClientInterface::~ClientInterface()
{ {
cancelRequest(); 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;
void ClientInterface::sendMessage( void ClientInterface::sendMessage(
const QString &message, const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useAgentMode)
{ {
cancelRequest(); cancelRequest();
m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits(); auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
auto attachFiles = m_contextManager->getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles); m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
auto &chatAssistantSettings = Settings::chatAssistantSettings(); auto &chatAssistantSettings = Settings::chatAssistantSettings();
@@ -84,7 +84,8 @@ void ClientInterface::sendMessage(
} }
auto templateName = Settings::generalSettings().caTemplate(); auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) { if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName)); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@@ -92,136 +93,52 @@ void ClientInterface::sendMessage(
} }
LLMCore::ContextData context; LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode; QString systemPrompt;
if (chatAssistantSettings.useSystemPrompt())
if (chatAssistantSettings.useSystemPrompt()) { systemPrompt = chatAssistantSettings.systemPrompt();
QString systemPrompt = chatAssistantSettings.systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString());
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (!linkedFiles.isEmpty()) { if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles); systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
} }
context.systemPrompt = systemPrompt;
}
QVector<LLMCore::Message> messages; QJsonObject providerRequest;
for (const auto &msg : m_chatModel->getChatHistory()) { providerRequest["model"] = Settings::generalSettings().caModel();
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) { providerRequest["stream"] = chatAssistantSettings.stream();
continue; providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
}
LLMCore::Message apiMessage; if (promptTemplate)
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant"; promptTemplate->prepareRequest(providerRequest, context);
apiMessage.content = msg.content; else
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking); qWarning("No prompt template found");
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
messages.append(apiMessage); if (provider)
} provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
context.history = messages; else
qWarning("No provider found");
LLMCore::LLMConfig config; LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat; config.requestType = LLMCore::RequestType::Chat;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
QString stream = QString{"streamGenerateContent?alt=sse"}; config.providerRequest = providerRequest;
config.url = QUrl(QString("%1/models/%2:%3") config.multiLineCompletion = false;
.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.apiKey = provider->apiKey(); config.apiKey = provider->apiKey();
config.provider->prepareRequest( QJsonObject request;
config.providerRequest, request["id"] = QUuid::createUuid().toString();
promptTemplate,
context,
LLMCore::RequestType::Chat,
isToolsEnabled,
Settings::chatAssistantSettings().enableThinkingMode());
QString requestId = QUuid::createUuid().toString(); auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
QJsonObject request{{"id", requestId}}; if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for chat request:");
LOG_MESSAGES(errors);
return;
}
m_activeRequests[requestId] = {request, provider}; m_requestHandler->sendLLMRequest(config, request);
emit requestStarted(requestId);
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);
connect(
provider,
&LLMCore::Provider::thinkingBlockReceived,
m_chatModel,
&ChatModel::addThinkingBlock,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::redactedThinkingBlockReceived,
m_chatModel,
&ChatModel::addRedactedThinkingBlock,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
} }
void ClientInterface::clearMessages() void ClientInterface::clearMessages()
@@ -232,37 +149,25 @@ void ClientInterface::clearMessages()
void ClientInterface::cancelRequest() void ClientInterface::cancelRequest()
{ {
QSet<LLMCore::Provider *> providers; auto id = m_chatModel->lastMessageId();
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { m_requestHandler->cancelRequest(id);
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
for (auto *provider : providers) {
disconnect(provider, nullptr, this, nullptr);
}
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");
} }
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request) void ClientInterface::handleLLMResponse(const QString &response,
const QJsonObject &request,
bool isComplete)
{ {
const auto message = response.trimmed(); const auto message = response.trimmed();
if (!message.isEmpty()) { if (!message.isEmpty()) {
QString messageId = request["id"].toString(); QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId); m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
if (isComplete) {
LOG_MESSAGE(
"Message completed. Final response for message " + messageId + ": " + response);
emit messageReceivedCompletely();
}
} }
} }
@@ -281,102 +186,30 @@ QString ClientInterface::getCurrentFileContext() const
} }
QString fileInfo = QString("Language: %1\nFile: %2\n\n") 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(); 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); return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
} }
QString ClientInterface::getSystemPromptWithLinkedFiles( QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
const QString &basePrompt, const QList<QString> &linkedFiles) const
{ {
QString updatedPrompt = basePrompt; QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) { if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n"; 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) { 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; 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);
}
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];
QString applyError;
bool applySuccess = Context::ChangesManager::instance()
.applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
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);
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
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 } // 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. * This file is part of QodeAssist.
* *
@@ -24,9 +24,7 @@
#include <QVector> #include <QVector>
#include "ChatModel.hpp" #include "ChatModel.hpp"
#include "Provider.hpp" #include "RequestHandler.hpp"
#include "llmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
@@ -35,49 +33,29 @@ class ClientInterface : public QObject
Q_OBJECT Q_OBJECT
public: public:
explicit ClientInterface( explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface(); ~ClientInterface();
void sendMessage( void sendMessage(
const QString &message, const QString &message,
const QList<QString> &attachments = {}, const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {}, const QList<QString> &linkedFiles = {});
bool useAgentMode = false);
void clearMessages(); void clearMessages();
void cancelRequest(); void cancelRequest();
Context::ContextManager *contextManager() const;
signals: signals:
void errorOccurred(const QString &error); void errorOccurred(const QString &error);
void messageReceivedCompletely(); void messageReceivedCompletely();
void requestStarted(const QString &requestId);
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: private:
void handleLLMResponse(const QString &response, const QJsonObject &request); void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
QString getCurrentFileContext() const; QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles( QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const; const QString &basePrompt,
const QList<QString> &linkedFiles) const;
struct RequestContext LLMCore::RequestHandler *m_requestHandler;
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
LLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel; ChatModel *m_chatModel;
Context::ContextManager *m_contextManager;
QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
}; };
} // namespace QodeAssist::Chat } // 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. * This file is part of QodeAssist.
* *
@@ -19,24 +19,33 @@
#pragma once #pragma once
#include <QObject> #include <qobject.h>
#include <QtQmlIntegration> #include <qqmlintegration.h>
#include "ChatData.hpp"
namespace QodeAssist::Chat { namespace QodeAssist::Chat {
Q_NAMESPACE
class MessagePart class MessagePart
{ {
Q_GADGET 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 text MEMBER text CONSTANT FINAL)
Q_PROPERTY(QString language MEMBER language CONSTANT FINAL) Q_PROPERTY(QString language MEMBER language CONSTANT FINAL)
QML_VALUE_TYPE(messagePart) QML_VALUE_TYPE(messagePart)
public: public:
MessagePartType type; enum PartType { Code, Text };
Q_ENUM(PartType)
PartType type;
QString text; QString text;
QString language; QString language;
}; };
class MessagePartType : public MessagePart
{
Q_GADGET
};
QML_NAMED_ELEMENT(MessagePart)
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
} // namespace QodeAssist::Chat } // namespace QodeAssist::Chat

View File

@@ -1,15 +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_74_61)">
<mask id="mask0_74_61" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_74_61)">
<path d="M8 22L18 32L36 12" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_61">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 548 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_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,17 +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_74_52)">
<mask id="mask0_74_52" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_74_52)">
<path d="M18 31C25.1797 31 31 25.1797 31 18C31 10.8203 25.1797 5 18 5C10.8203 5 5 10.8203 5 18C5 25.1797 10.8203 31 18 31Z" stroke="black" stroke-width="3.5"/>
<path d="M27 27L38 38" stroke="black" stroke-width="3.5" stroke-linecap="round"/>
<path d="M16.375 23L18.2841 11.3636H20.1023L18.1932 23H16.375ZM11.1648 20.1136L11.4659 18.2955H20.5568L20.2557 20.1136H11.1648ZM12.2841 23L14.1932 11.3636H16.0114L14.1023 23H12.2841ZM11.8295 16.0682L12.1364 14.25H21.2273L20.9205 16.0682H11.8295Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_52">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 943 B

View File

@@ -1,16 +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_74_76)">
<mask id="mask0_74_76" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_74_76)">
<path d="M12 12L32 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
<path d="M32 12L12 32" stroke="black" stroke-width="4" stroke-linecap="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_76">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 599 B

View File

@@ -1,9 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.75 2.75H8.25C6.73122 2.75 5.5 3.98122 5.5 5.5V38.5C5.5 40.0188 6.73122 41.25 8.25 41.25H35.75C37.2688 41.25 38.5 40.0188 38.5 38.5V5.5C38.5 3.98122 37.2688 2.75 35.75 2.75Z" stroke="black" stroke-width="4"/>
<path d="M13.75 14.4375C14.8891 14.4375 15.8125 13.5141 15.8125 12.375C15.8125 11.2359 14.8891 10.3125 13.75 10.3125C12.6109 10.3125 11.6875 11.2359 11.6875 12.375C11.6875 13.5141 12.6109 14.4375 13.75 14.4375Z" fill="black"/>
<path d="M19.25 12.375H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.75 24.0625C14.8891 24.0625 15.8125 23.1391 15.8125 22C15.8125 20.8609 14.8891 19.9375 13.75 19.9375C12.6109 19.9375 11.6875 20.8609 11.6875 22C11.6875 23.1391 12.6109 24.0625 13.75 24.0625Z" fill="black"/>
<path d="M19.25 22H33" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M13.75 33.6875C14.8891 33.6875 15.8125 32.7641 15.8125 31.625C15.8125 30.4859 14.8891 29.5625 13.75 29.5625C12.6109 29.5625 11.6875 30.4859 11.6875 31.625C11.6875 32.7641 12.6109 33.6875 13.75 33.6875Z" fill="black"/>
<path d="M19.25 31.625H27.5" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

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,4 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1,3 +0,0 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.4445 9.32233C17.7036 7.28556 21.8559 7.75441 25.8713 9.68854C27.428 9.4057 30.1744 8.91006 31.6477 9.47565C34.5351 10.5309 36.6339 12.7385 37.0285 14.9805C37.81 15.3756 38.4502 15.9932 38.8635 16.751C39.7282 18.3354 39.8498 19.9232 39.2678 21.2061C39.8159 22.277 39.9974 23.4913 39.7844 24.67C39.663 25.4561 39.3556 26.2047 38.8869 26.8555C38.4183 27.5062 37.8013 28.0419 37.0842 28.42C36.8857 28.5274 34.5887 28.6167 34.3713 28.6885C34.6443 32.2168 30.9868 33.5005 27.8889 32.6602L29.0403 36.586L26.0803 36.6885L23.8713 31.6885L21.8713 29.6885C20.125 30.1697 17.0919 30.168 15.76 28.0831C15.639 27.8916 15.5299 27.693 15.4319 27.4893C15.0931 27.5567 14.7474 27.5909 14.4016 27.5919C13.415 27.5918 11.771 27.3037 10.9358 26.7393C10.2736 26.3112 9.74862 25.7095 9.42014 25.004C7.64097 25.2413 6.13134 24.8334 5.14474 23.8262C3.8951 22.5721 3.72021 18.9738 4.37131 16.751C5.22965 13.7841 7.6818 12.9427 12.8713 11.6885C13.3214 11.1426 13.8387 9.69851 14.4445 9.32233ZM21.2551 15.0001L20.9358 16.1114L19.8723 16.4444L19.3401 15.5557L18.4895 16.3331L19.0217 17.2217L18.383 18.2217L17.2131 18.0001L17.0002 18.8887L18.0637 19.4444V20.5557L17.0002 21.1114L17.2131 22.0001L18.383 21.7774L19.0217 22.7774L18.4895 23.6671L19.3401 24.4444L19.8723 23.5557L20.9358 23.8887L21.2551 25.0001H22.7444L23.0637 23.8887L24.1272 23.5557L24.6594 24.4444L25.511 23.6671L24.9787 22.7774L25.6174 21.7774L26.7873 22.0001L27.0002 21.1114L25.9358 20.5557V19.4444L27.0002 18.8887L26.7873 18.0001L25.6174 18.2217L24.9787 17.2217L25.6174 16.4444L24.6594 15.5557L24.1272 16.4444L23.0637 16.1114L22.7444 15.0001H21.2551Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,16 +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_74_68)">
<mask id="mask0_74_68" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="44" height="44">
<path d="M44 0H0V44H44V0Z" fill="white"/>
</mask>
<g mask="url(#mask0_74_68)">
<path d="M12 12L6 18L12 24" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 18H28C33 18 38 23 38 28C38 33 33 38 28 38H22" stroke="black" stroke-width="4" stroke-linecap="round"/>
</g>
</g>
<defs>
<clipPath id="clip0_74_68">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 663 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. * This file is part of QodeAssist.
* *
@@ -23,7 +23,6 @@ Rectangle {
id: root id: root
property alias text: badgeText.text property alias text: badgeText.text
property alias hovered: mouse.hovered
implicitWidth: badgeText.implicitWidth + root.radius implicitWidth: badgeText.implicitWidth + root.radius
implicitHeight: badgeText.implicitHeight + 6 implicitHeight: badgeText.implicitHeight + 6
@@ -38,10 +37,4 @@ Rectangle {
anchors.centerIn: parent anchors.centerIn: parent
color: palette.buttonText 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. * This file is part of QodeAssist.
* *
@@ -19,10 +19,7 @@
import QtQuick import QtQuick
import ChatView import ChatView
import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import UIControls
import "./dialog" import "./dialog"
Rectangle { Rectangle {
@@ -30,42 +27,14 @@ Rectangle {
property alias msgModel: msgCreator.model property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.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 height: msgColumn.implicitHeight + 10
radius: 8 radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout { ColumnLayout {
id: msgColumn id: msgColumn
x: 5 width: parent.width
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: 5 spacing: 5
@@ -87,8 +56,8 @@ Rectangle {
} }
switch(modelData.type) { switch(modelData.type) {
case MessagePartType.Text: return textComponent; case MessagePart.Text: return textComponent;
case MessagePartType.Code: return codeBlockComponent; case MessagePart.Code: return codeBlockComponent;
default: return textComponent; default: return textComponent;
} }
} }
@@ -144,66 +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
}
icon {
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
height: 15
width: 15
}
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Reset chat to this message and edit")
ToolTip.delay: 500
}
component TextComponent : TextBlock { component TextComponent : TextBlock {
required property var itemData required property var itemData
height: implicitHeight + 10 height: implicitHeight + 10
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
leftPadding: 10 leftPadding: 10
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text) text: 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
}
}
component CodeBlockComponent : CodeBlock { component CodeBlockComponent : CodeBlock {
id: codeblock
required property var itemData required property var itemData
anchors { anchors {
left: parent.left left: parent.left
@@ -214,7 +133,5 @@ Rectangle {
code: itemData.text code: itemData.text
language: itemData.language language: itemData.language
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
} }
} }

View File

@@ -1,469 +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 QtQuick.Layouts
import UIControls
import ChatView
import Qt.labs.platform as Platform
Rectangle {
id: root
property string editContent: ""
readonly property var editData: parseEditData(editContent)
readonly property string filePath: editData.file || ""
readonly property string fileName: getFileName(filePath)
readonly property string editStatus: editData.status || "pending"
readonly property string statusMessage: editData.status_message || ""
readonly property string oldContent: editData.old_content || ""
readonly property string newContent: editData.new_content || ""
signal applyEdit(string editId)
signal rejectEdit(string editId)
signal undoEdit(string editId)
signal openInEditor(string editId)
readonly property int borderRadius: 4
readonly property int contentMargin: 10
readonly property int contentBottomPadding: 20
readonly property int headerPadding: 8
readonly property int statusIndicatorWidth: 4
readonly property bool isPending: editStatus === "pending"
readonly property bool isApplied: editStatus === "applied"
readonly property bool isRejected: editStatus === "rejected"
readonly property bool isArchived: editStatus === "archived"
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
readonly property color pendingColor: palette.highlight
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
readonly property string codeFontFamily: {
switch (Qt.platform.os) {
case "windows": return "Consolas"
case "osx": return "Menlo"
case "linux": return "DejaVu Sans Mono"
default: return "monospace"
}
}
readonly property int codeFontSize: Qt.application.font.pointSize
readonly property color statusColor: {
if (isArchived) return archivedColor
if (isApplied) return appliedColor
if (isRejected) return rejectedColor
return pendingColor
}
readonly property color statusBgColor: {
if (isArchived) return archivedBgColor
if (isApplied) return appliedBgColor
if (isRejected) return rejectedBgColor
return palette.button
}
readonly property string statusText: {
if (isArchived) return qsTr("ARCHIVED")
if (isApplied) return qsTr("APPLIED")
if (isRejected) return qsTr("REJECTED")
return qsTr("PENDING")
}
readonly property int addedLines: countLines(newContent)
readonly property int removedLines: countLines(oldContent)
function parseEditData(content) {
try {
const marker = "QODEASSIST_FILE_EDIT:";
let jsonStr = content;
if (content.indexOf(marker) >= 0) {
jsonStr = content.substring(content.indexOf(marker) + marker.length);
}
return JSON.parse(jsonStr);
} catch (e) {
return {
edit_id: "",
file: "",
old_content: "",
new_content: "",
status: "error",
status_message: ""
};
}
}
function getFileName(path) {
if (!path) return "";
const parts = path.split('/');
return parts[parts.length - 1];
}
function countLines(text) {
if (!text) return 0;
return text.split('\n').length;
}
implicitHeight: fileEditView.implicitHeight
ChatUtils {
id: utils
}
Rectangle {
id: fileEditView
property bool expanded: false
anchors.fill: parent
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
: headerArea.height
radius: root.borderRadius
color: palette.base
border.width: 1
border.color: root.isPending
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
: Qt.alpha(root.statusColor, 0.6)
clip: true
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
states: [
State {
name: "expanded"
when: fileEditView.expanded
PropertyChanges { target: contentColumn; opacity: 1 }
},
State {
name: "collapsed"
when: !fileEditView.expanded
PropertyChanges { target: contentColumn; opacity: 0 }
}
]
transitions: Transition {
NumberAnimation {
properties: "opacity"
duration: 200
easing.type: Easing.InOutQuad
}
}
MouseArea {
id: headerArea
width: parent.width
height: headerRow.height + 16
cursorShape: Qt.PointingHandCursor
onClicked: fileEditView.expanded = !fileEditView.expanded
RowLayout {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: actionButtons.left
leftMargin: root.contentMargin
rightMargin: root.contentMargin
}
spacing: root.headerPadding
Rectangle {
width: root.statusIndicatorWidth
height: headerText.height
radius: 2
color: root.statusColor
}
Text {
id: headerText
Layout.fillWidth: true
text: {
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
if (root.oldContent.length > 0) {
return qsTr("%1: %2 (+%3 -%4)")
.arg(modeText)
.arg(root.fileName)
.arg(root.addedLines)
.arg(root.removedLines)
} else {
return qsTr("%1: %2 (+%3)")
.arg(modeText)
.arg(root.fileName)
.arg(root.addedLines)
}
}
font.pixelSize: 12
font.bold: true
color: palette.text
elide: Text.ElideMiddle
}
Text {
text: fileEditView.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
Rectangle {
visible: !root.isPending
Layout.preferredWidth: badgeText.width + 12
Layout.preferredHeight: badgeText.height + 4
color: root.statusBgColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: root.statusText
font.pixelSize: 9
font.bold: true
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
}
}
}
Row {
id: actionButtons
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 6
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
height: 15
width: 15
}
hoverEnabled: true
onClicked: root.openInEditor(editData.edit_id)
ToolTip.visible: hovered
ToolTip.text: qsTr("Open file in editor and navigate to changes")
ToolTip.delay: 500
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
height: 15
width: 15
} enabled: (root.isPending || root.isRejected) && !root.isArchived
visible: !root.isApplied && !root.isArchived
onClicked: root.applyEdit(editData.edit_id)
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
height: 15
width: 15
}
enabled: root.isApplied && !root.isArchived
visible: root.isApplied && !root.isArchived
onClicked: root.undoEdit(editData.edit_id)
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
height: 15
width: 15
}
enabled: root.isPending && !root.isArchived
visible: root.isPending && !root.isArchived
onClicked: root.rejectEdit(editData.edit_id)
}
}
}
ColumnLayout {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: headerArea.bottom
margins: root.contentMargin
}
spacing: 8
visible: opacity > 0
Text {
Layout.fillWidth: true
text: root.filePath
font.pixelSize: 10
color: palette.mid
elide: Text.ElideMiddle
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
color: Qt.rgba(1, 0.2, 0.2, 0.1)
radius: 4
border.width: 1
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
visible: root.oldContent.length > 0
Column {
id: oldContentColumn
width: parent.width
x: 6
y: 6
spacing: 4
TextEdit {
id: oldContentText
width: parent.width - 12
height: contentHeight
text: root.oldContent
font.family: root.codeFontFamily
font.pixelSize: root.codeFontSize
color: palette.text
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectByKeyboard: true
textFormat: TextEdit.PlainText
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: oldConentContextMenu.open()
}
}
Platform.Menu {
id: oldConentContextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = oldContentText.selectedText || root.oldContent
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: fileEditView.expanded = !fileEditView.expanded
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: newContentColumn.implicitHeight + 12
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
radius: 4
border.width: 1
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
Column {
id: newContentColumn
width: parent.width
x: 6
y: 6
spacing: 4
TextEdit {
id: newContentText
width: parent.width - 12
height: contentHeight
text: root.newContent
font.family: root.codeFontFamily
font.pixelSize: root.codeFontSize
color: palette.text
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectByKeyboard: true
textFormat: TextEdit.PlainText
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: newContentContextMenu.open()
}
}
Platform.Menu {
id: newContentContextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = newContentText.selectedText || root.newContent
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: fileEditView.expanded = !fileEditView.expanded
}
}
}
}
Text {
Layout.fillWidth: true
visible: root.statusMessage.length > 0
text: root.statusMessage
font.pixelSize: 10
font.italic: true
color: root.isApplied
? Qt.rgba(0.2, 0.6, 0.2, 1)
: Qt.rgba(0.8, 0.2, 0.2, 1)
wrapMode: Text.WordWrap
}
}
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -22,8 +22,7 @@ import QtQuick.Controls
import QtQuick.Controls.Basic as QQC import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import UIControls import "./controls"
import Qt.labs.platform as Platform
import "./parts" import "./parts"
ChatRootView { ChatRootView {
@@ -65,80 +64,40 @@ ChatRootView {
id: topBar id: topBar
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + 10 Layout.preferredHeight: 40
saveButton.onClicked: root.showSaveDialog() saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog() loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat() clearButton.onClicked: root.clearChat()
tokensBadge { 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 { recentPath {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved") text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
} }
openChatHistory.onClicked: root.openChatHistoryFolder() openChatHistory.onClicked: root.openChatHistoryFolder()
rulesButton.onClicked: rulesViewer.open()
activeRulesCount: root.activeRulesCount
pinButton {
visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
agentModeSwitch {
checked: root.isAgentMode
enabled: root.toolsSupportEnabled
onToggled: {
root.isAgentMode = agentModeSwitch.checked
}
}
thinkingMode {
checked: root.isThinkingMode
enabled: root.isThinkingSupport
onCheckedChanged: {
root.isThinkingMode = thinkingMode.checked
}
}
} }
ListView { ListView {
id: chatListView id: chatListView
signal hideServiceComponents(int itemIndex)
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
leftMargin: 5 leftMargin: 5
model: root.chatModel model: root.chatModel
clip: true clip: true
spacing: 0 spacing: 10
boundsBehavior: Flickable.StopAtBounds boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000 cacheBuffer: 2000
delegate: Loader { delegate: ChatItem {
id: componentLoader
required property var model required property var model
required property int index
width: ListView.view.width - scroll.width width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content)
sourceComponent: { messageAttachments: model.attachments
if (model.roleType === ChatModel.Tool) { color: model.roleType === ChatModel.User ? palette.alternateBase
return toolMessageComponent : palette.base
} else if (model.roleType === ChatModel.FileEdit) {
return fileEditMessageComponent
} else if (model.roleType === ChatModel.Thinking) {
return thinkingMessageComponent
} else {
return chatItemComponent
}
}
onLoaded: {
if (componentLoader.sourceComponent == chatItemComponent) {
chatListView.hideServiceComponents(index)
}
}
} }
header: Item { header: Item {
@@ -159,104 +118,6 @@ ChatRootView {
root.scrollToBottom() root.scrollToBottom()
} }
} }
Component {
id: chatItemComponent
ChatItem {
id: chatItemInstance
width: parent.width
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 {
id: toolsItem
width: parent.width
toolContent: model.content
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
toolsItem.headerOpacity = 0.5
}
}
}
}
}
Component {
id: fileEditMessageComponent
FileEditItem {
width: parent.width
editContent: model.content
onApplyEdit: function(editId) {
root.applyFileEdit(editId)
}
onRejectEdit: function(editId) {
root.rejectFileEdit(editId)
}
onUndoEdit: function(editId) {
root.undoFileEdit(editId)
}
onOpenInEditor: function(editId) {
root.openFileEditInEditor(editId)
}
}
}
Component {
id: thinkingMessageComponent
ThinkingStatusItem {
id: thinking
width: parent.width
thinkingContent: {
let content = model.content
let signatureStart = content.indexOf("\n[Signature:")
if (signatureStart >= 0) {
return content.substring(0, signatureStart)
}
return content
}
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
thinking.headerOpacity = 0.5
}
}
}
}
}
} }
ScrollView { ScrollView {
@@ -269,9 +130,7 @@ ChatRootView {
QQC.TextArea { QQC.TextArea {
id: messageInput id: messageInput
placeholderText: Qt.platform.os === "osx" placeholderText: qsTr("Type your message here...")
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderTextColor: palette.mid placeholderTextColor: palette.mid
color: palette.text color: palette.text
background: Rectangle { background: Rectangle {
@@ -294,51 +153,13 @@ ChatRootView {
onTextChanged: root.calculateMessageTokensCount(messageInput.text) onTextChanged: root.calculateMessageTokensCount(messageInput.text)
MouseArea { Keys.onPressed: function(event) {
anchors.fill: parent if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
acceptedButtons: Qt.RightButton root.sendChatMessage()
onClicked: messageContextMenu.open() event.accepted = true;
propagateComposedEvents: 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 { AttachedFilesPlace {
@@ -363,49 +184,22 @@ ChatRootView {
onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index) onRemoveFileFromListByIndex: (index) => root.removeFileFromLinkList(index)
} }
FileEditsActionBar {
id: fileEditsActionBar
Layout.fillWidth: true
totalEdits: root.currentMessageTotalEdits
appliedEdits: root.currentMessageAppliedEdits
pendingEdits: root.currentMessagePendingEdits
rejectedEdits: root.currentMessageRejectedEdits
onApplyAllClicked: root.applyAllFileEditsForCurrentMessage()
onUndoAllClicked: root.undoAllFileEditsForCurrentMessage()
}
BottomBar { BottomBar {
id: bottomBar id: bottomBar
Layout.preferredWidth: parent.width Layout.preferredWidth: parent.width
Layout.preferredHeight: 40 Layout.preferredHeight: 40
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage() sendButton.onClicked: root.sendChatMessage()
: root.cancelRequest() stopButton.onClicked: 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")
syncOpenFiles { syncOpenFiles {
checked: root.isSyncOpenFiles checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked) onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
} }
attachFiles.onClicked: root.showAttachFilesDialog() attachFiles.onClicked: root.showAttachFilesDialog()
linkFiles.onClicked: root.showLinkFilesDialog() linkFiles.onClicked: root.showLinkFilesDialog()
} testRag.onClicked: root.testRAG(messageInput.text)
} testChunks.onClicked: root.testChunking()
Shortcut {
id: sendMessageShortcut
sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut
onActivated: {
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
root.sendChatMessage()
}
} }
} }
@@ -424,55 +218,4 @@ ChatRootView {
messageInput.text = "" messageInput.text = ""
scrollToBottom() scrollToBottom()
} }
Toast {
id: errorToast
z: 1000
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
Toast {
id: infoToast
z: 1000
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
RulesViewer {
id: rulesViewer
width: parent.width * 0.8
height: parent.height * 0.8
x: (parent.width - width) / 2
y: (parent.height - height) / 2
activeRules: root.activeRules
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
onRefreshRules: root.refreshRules()
onOpenRulesFolder: root.openRulesFolder()
}
Connections {
target: root
function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage)
}
}
function onLastInfoMessageChanged() {
if (root.lastInfoMessage.length > 0) {
infoToast.show(root.lastInfoMessage)
}
}
}
Component.onCompleted: {
messageInput.forceActiveFocus()
}
} }

View File

@@ -1,183 +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 thinkingContent: ""
// property string signature: ""
property bool isRedacted: false
property bool expanded: false
property alias headerOpacity: headerRow.opacity
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: root.isRedacted ? qsTr("Thinking (Redacted)")
: qsTr("Thinking")
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
Text {
visible: root.isRedacted
width: parent.width
text: qsTr("Thinking content was redacted by safety systems")
font.pixelSize: 11
font.italic: true
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
wrapMode: Text.WordWrap
}
TextEdit {
id: thinkingText
visible: !root.isRedacted
width: parent.width
text: root.thinkingContent
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
// Rectangle {
// visible: root.signature.length > 0 && root.expanded
// width: parent.width
// height: signatureText.height + 10
// color: palette.alternateBase
// radius: 4
// Text {
// id: signatureText
// anchors {
// left: parent.left
// right: parent.right
// verticalCenter: parent.verticalCenter
// margins: 5
// }
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
// font.pixelSize: 9
// font.family: "monospace"
// color: palette.mid
// elide: Text.ElideRight
// }
// }
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: thinkingMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
: (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,162 +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
property alias headerOpacity: headerRow.opacity
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
width: parent.width
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. * 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. * This file is part of QodeAssist.
* *
@@ -20,127 +20,71 @@
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import ChatView import ChatView
import UIControls
import Qt.labs.platform as Platform
Rectangle { Rectangle {
id: root id: root
property string code: "" property string code: ""
property string language: "" property string language: ""
property bool expanded: false
property alias codeFontFamily: codeText.font.family readonly property string monospaceFont: {
property alias codeFontSize: codeText.font.pointSize switch (Qt.platform.os) {
readonly property real collapsedHeight: copyButton.height + 10 case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
}
}
color: palette.alternateBase color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3) border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3) : Qt.lighter(root.color, 1.3)
border.width: 2 border.width: 2
radius: 4 radius: 4
implicitWidth: parent.width
clip: true
Behavior on implicitHeight { implicitWidth: parent.width
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad } implicitHeight: codeText.implicitHeight + 20
}
ChatUtils { ChatUtils {
id: utils 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 { TextEdit {
id: codeText id: codeText
anchors { anchors.fill: parent
left: parent.left anchors.margins: 10
right: parent.right
top: header.bottom
margins: 10
}
text: root.code text: root.code
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
font.family: root.monospaceFont
font.pointSize: Qt.application.font.pointSize
color: parent.color.hslLightness > 0.5 ? "black" : "white" color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
selectionColor: palette.highlight selectionColor: palette.highlight
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
}
} }
Platform.Menu { TextEdit {
id: contextMenu anchors.top: parent.top
anchors.right: parent.right
Platform.MenuItem { anchors.margins: 5
text: qsTr("Copy") readOnly: true
onTriggered: { selectByMouse: true
const textToCopy = codeText.selectedText || root.code text: root.language
utils.copyToClipboard(textToCopy) color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
} : Qt.lighter(root.color, 1.1)
} font.pointSize: 8
Platform.MenuSeparator {}
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
} }
QoAButton { QoAButton {
id: copyButton anchors.top: parent.top
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: 5 anchors.margins: 5
text: "Copy"
y: 5
text: qsTr("Copy")
onClicked: { onClicked: {
utils.copyToClipboard(root.code) utils.copyToClipboard(root.code)
text = qsTr("Copied") text = qsTr("Copied")
@@ -153,21 +97,4 @@ Rectangle {
onTriggered: parent.text = qsTr("Copy") 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. * This file is part of QodeAssist.
* *
@@ -18,7 +18,6 @@
*/ */
import QtQuick import QtQuick
import Qt.labs.platform as Platform
TextEdit { TextEdit {
id: root id: root
@@ -26,31 +25,7 @@ TextEdit {
readOnly: true readOnly: true
selectByMouse: true selectByMouse: true
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
textFormat: Text.StyledText
selectionColor: palette.highlight selectionColor: palette.highlight
color: palette.text color: palette.text
onLinkActivated: (link) => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
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. * 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. * This file is part of QodeAssist.
* *
@@ -21,16 +21,17 @@ import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import ChatView import ChatView
import UIControls
Rectangle { Rectangle {
id: root id: root
property alias sendButton: sendButtonId property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId property alias attachFiles: attachFilesId
property alias linkFiles: linkFilesId property alias linkFiles: linkFilesId
property alias testRag: testRagId
property alias testChunks: testChunksId
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
@@ -52,12 +53,13 @@ Rectangle {
QoAButton { QoAButton {
id: sendButtonId id: sendButtonId
icon { text: qsTr("Send")
height: 15
width: 15
} }
ToolTip.visible: hovered
ToolTip.delay: 250 QoAButton {
id: stopButtonId
text: qsTr("Stop")
} }
QoAButton { QoAButton {
@@ -68,9 +70,7 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered text: qsTr("Attach files")
ToolTip.delay: 250
ToolTip.text: qsTr("Attach file to message")
} }
QoAButton { QoAButton {
@@ -81,9 +81,7 @@ Rectangle {
height: 15 height: 15
width: 8 width: 8
} }
ToolTip.visible: hovered text: qsTr("Link files")
ToolTip.delay: 250
ToolTip.text: qsTr("Link file to context")
} }
CheckBox { CheckBox {
@@ -95,6 +93,18 @@ Rectangle {
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context") 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 { Item {
Layout.fillWidth: true Layout.fillWidth: true
} }

View File

@@ -1,161 +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 QtQuick.Layouts
import UIControls
Rectangle {
id: root
property int totalEdits: 0
property int appliedEdits: 0
property int pendingEdits: 0
property int rejectedEdits: 0
property bool hasAppliedEdits: appliedEdits > 0
property bool hasRejectedEdits: rejectedEdits > 0
property bool hasPendingEdits: pendingEdits > 0
signal applyAllClicked()
signal undoAllClicked()
visible: totalEdits > 0
implicitHeight: visible ? 40 : 0
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.05) :
Qt.lighter(palette.window, 1.05)
border.width: 1
border.color: palette.mid
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
RowLayout {
anchors {
left: parent.left
leftMargin: 10
right: parent.right
rightMargin: 10
verticalCenter: parent.verticalCenter
}
spacing: 10
Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
radius: 12
color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
}
border.width: 2
border.color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
}
Text {
anchors.centerIn: parent
text: root.totalEdits
font.pixelSize: 10
font.bold: true
color: palette.text
}
}
// Status text
ColumnLayout {
spacing: 2
Text {
text: root.totalEdits === 1
? qsTr("File Edit in Current Message")
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
font.pixelSize: 11
font.bold: true
color: palette.text
}
Text {
visible: root.totalEdits > 0
text: {
let parts = [];
if (root.appliedEdits > 0) {
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
}
if (root.pendingEdits > 0) {
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
}
if (root.rejectedEdits > 0) {
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
}
return parts.join(", ");
}
font.pixelSize: 9
color: palette.mid
}
}
Item {
Layout.fillWidth: true
}
QoAButton {
id: applyAllButton
visible: root.hasPendingEdits || root.hasRejectedEdits
enabled: root.hasPendingEdits || root.hasRejectedEdits
text: root.hasPendingEdits
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.hasPendingEdits
? qsTr("Apply all pending and rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
onClicked: root.applyAllClicked()
}
QoAButton {
id: undoAllButton
visible: root.hasAppliedEdits
enabled: root.hasAppliedEdits
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Undo all applied edits in this message")
onClicked: root.undoAllClicked()
}
}
}

View File

@@ -1,251 +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 QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property var activeRules
property alias rulesCurrentIndex: rulesList.currentIndex
property alias ruleContentAreaText: ruleContentArea.text
signal refreshRules()
signal openRulesFolder()
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Active Project Rules")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Open Folder")
onClicked: root.openRulesFolder()
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
SplitView {
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Horizontal
Rectangle {
SplitView.minimumWidth: 200
SplitView.preferredWidth: parent.width * 0.3
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules Files (%1)").arg(rulesList.count)
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted) {
return palette.highlight
} else if (parent.hovered) {
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
}
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 11
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: qsTr("Category: %1").arg(modelData.category)
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
}
}
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
}
Text {
visible: rulesList.count === 0
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
font.pixelSize: 10
color: palette.mid
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
}
}
}
Rectangle {
SplitView.fillWidth: true
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: ruleContentArea.text.length > 0
onClicked: utils.copyToClipboard(ruleContentArea.text)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
TextEdit {
id: ruleContentArea
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
}
}
}
}
Text {
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}

View File

@@ -1,103 +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: root
property alias toastTextItem: textItem
property alias toastTextColor: textItem.color
property string errorText: ""
property int displayDuration: 7000
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
height: visible ? (textItem.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: textItem
anchors.centerIn: parent
anchors.margins: 6
text: root.errorText
color: palette.text
font.pixelSize: 13
wrapMode: TextEdit.Wrap
width: Math.min(implicitWidth, root.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: root
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
id: hideAnimation
target: root
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InQuad
onFinished: root.visible = false
}
Timer {
id: hideTimer
interval: root.displayDuration
running: false
repeat: false
onTriggered: root.hide()
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,9 +19,7 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls
import ChatView import ChatView
import UIControls
Rectangle { Rectangle {
id: root id: root
@@ -32,212 +30,59 @@ Rectangle {
property alias tokensBadge: tokensBadgeId property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias rulesButton: rulesButtonId
property alias agentModeSwitch: agentModeSwitchId
property alias thinkingMode: thinkingModeId
property alias activeRulesCount: activeRulesCountId.text
color: palette.window.hslLightness > 0.5 ? color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) : Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1) Qt.lighter(palette.window, 1.1)
Flow { RowLayout {
anchors { anchors {
left: parent.left left: parent.left
leftMargin: 5
right: parent.right right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
margins: 5
} }
spacing: 10
Row {
height: agentModeSwitchId.height
spacing: 10
QoAButton {
id: pinButtonId
anchors.verticalCenter: parent.verticalCenter
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")
}
QoATextSlider {
id: agentModeSwitchId
anchors.verticalCenter: parent.verticalCenter
leftText: "chat"
rightText: "AI Agent"
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!agentModeSwitchId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return checked
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
: qsTr("Chat Mode: Simple conversation without tool access")
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
Item {
height: agentModeSwitchId.height
width: recentPathId.width
Text {
id: recentPathId
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, root.width)
elide: Text.ElideMiddle
color: palette.text
font.pixelSize: 12
MouseArea {
anchors.fill: parent
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: recentPathId.text
}
}
}
RowLayout {
Layout.preferredWidth: root.width
spacing: 10 spacing: 10
QoAButton { QoAButton {
id: saveButtonId id: saveButtonId
icon { text: qsTr("Save")
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")
} }
QoAButton { QoAButton {
id: loadButtonId id: loadButtonId
icon { text: qsTr("Load")
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")
} }
QoAButton { QoAButton {
id: clearButtonId id: clearButtonId
icon { text: qsTr("Clear")
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
} }
ToolTip.visible: hovered
ToolTip.delay: 250 Text {
ToolTip.text: qsTr("Clean chat") id: recentPathId
elide: Text.ElideMiddle
color: palette.text
} }
QoAButton { QoAButton {
id: openChatHistoryId id: openChatHistoryId
icon { text: qsTr("Show in system")
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")
} }
QoAButton { Item {
id: rulesButtonId Layout.fillWidth: true
icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
height: 15
width: 15
}
text: " "
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
: qsTr("View active project rules (no rules found)")
Text {
id: activeRulesCountId
anchors {
bottom: parent.bottom
bottomMargin: 2
right: parent.right
rightMargin: 4
}
color: palette.text
font.pixelSize: 10
font.bold: true
}
} }
Badge { Badge {
id: tokensBadgeId 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) 2024 Petr Mironychev
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -19,144 +18,29 @@
*/ */
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash> #include <QHash>
namespace QodeAssist { namespace QodeAssist {
struct LanguageProperties QString CodeHandler::processText(QString text)
{
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 result; QString result;
QStringList lines = text.split('\n'); QStringList lines = text.split('\n');
bool inCodeBlock = false; bool inCodeBlock = false;
QString pendingComments; QString pendingComments;
QString currentLanguage;
auto currentFileExtension = QFileInfo(currentFilePath).suffix(); for (const QString &line : lines) {
auto currentLanguage = detectLanguageFromExtension(currentFileExtension); if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto addPendingCommentsIfAny = [&]() { currentLanguage = detectLanguage(line);
if (pendingComments.isEmpty()) {
return;
} }
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
if (!pendingComments.isEmpty()) {
QStringList commentLines = pendingComments.split('\n'); QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage); QString commentPrefix = getCommentPrefix(currentLanguage);
@@ -168,28 +52,7 @@ QString CodeHandler::processText(QString text, QString currentFilePath)
} }
} }
pendingComments.clear(); pendingComments.clear();
};
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 ```
}
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
result += line + "\n"; result += line + "\n";
} else { } else {
QString trimmed = line.trimmed(); 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; return result;
} }
QString CodeHandler::getCommentPrefix(const QString &language) QString CodeHandler::getCommentPrefix(const QString &language)
{ {
static const auto commentPrefixes = buildLanguageToCommentPrefixMap(); static const QHash<QString, QString> commentPrefixes
return commentPrefixes.value(language, "//"); = {{"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(); QString trimmed = line.trimmed();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), ""); if (trimmed.length() <= 3) { // Если только ```
} return QString();
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension) return trimmed.mid(3).trimmed();
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
} }
const QRegularExpression &CodeHandler::getFullCodeBlockRegex() 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. * This file is part of QodeAssist.
* *
@@ -28,25 +28,11 @@ namespace QodeAssist {
class CodeHandler class CodeHandler
{ {
public: public:
static QString processText(QString text, QString currentFileName); static QString processText(QString text);
/**
* 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);
private: private:
static QString getCommentPrefix(const QString &language); static QString getCommentPrefix(const QString &language);
static QString detectLanguage(const QString &line);
static const QRegularExpression &getFullCodeBlockRegex(); static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex(); 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. * This file is part of QodeAssist.
* *
@@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp" #include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer> #include <QTimer>
#include <settings/ButtonAspect.hpp>
#include "QodeAssisttr.h" #include "QodeAssisttr.h"
@@ -35,52 +35,6 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init() void ConfigurationManager::init()
{ {
setupConnections(); 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());
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
m_generalSettings.qrTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
updateTemplateDescription(m_generalSettings.qrTemplate);
}
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) ConfigurationManager::ConfigurationManager(QObject *parent)
@@ -97,16 +51,12 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider); connect(&m_generalSettings.ccSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider); connect(&m_generalSettings.caSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.qrSelectProvider, &Button::clicked, this, &Config::selectProvider);
connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.ccSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.caSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.qrSelectModel, &Button::clicked, this, &Config::selectModel);
connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.ccSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate); connect(&m_generalSettings.caSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.qrSelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.ccSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl); connect(&m_generalSettings.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
connect( connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider); &m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
@@ -114,18 +64,6 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel); connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect( connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate); &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);
});
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.qrTemplate);
});
} }
void ConfigurationManager::selectProvider() void ConfigurationManager::selectProvider()
@@ -140,13 +78,13 @@ void ConfigurationManager::selectProvider()
? m_generalSettings.ccProvider ? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider : settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider ? m_generalSettings.ccPreset1Provider
: settingsButton == &m_generalSettings.qrSelectProvider
? m_generalSettings.qrProvider
: m_generalSettings.caProvider; : m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] { QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(providersList,
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:")); targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
}); });
} }
@@ -158,21 +96,17 @@ void ConfigurationManager::selectModel()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel); const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue() const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue() : isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue(); : m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue() const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue() : isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue(); : m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model : isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel; : m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) { if (auto provider = m_providersManager.getProviderByName(providerName)) {
@@ -203,25 +137,19 @@ void ConfigurationManager::selectTemplate()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate); const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate); const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
? m_templateManger.getFimTemplatesForProvider(providerID) : m_templateManger.chatTemplatesNames();
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template : isPreset1 ? m_generalSettings.ccPreset1Template
: isQuickRefactor ? m_generalSettings.qrTemplate
: m_generalSettings.caTemplate; : m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog( m_generalSettings.showSelectionDialog(templateList,
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:")); targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
}); });
} }
@@ -241,8 +169,6 @@ void ConfigurationManager::selectUrl()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
: settingsButton == &m_generalSettings.ccPreset1SetUrl : settingsButton == &m_generalSettings.ccPreset1SetUrl
? m_generalSettings.ccPreset1Url ? m_generalSettings.ccPreset1Url
: settingsButton == &m_generalSettings.qrSetUrl
? m_generalSettings.qrUrl
: m_generalSettings.caUrl; : m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() { QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2024-2025 Petr Mironychev * Copyright (C) 2024 Petr Mironychev
* *
* This file is part of QodeAssist. * This file is part of QodeAssist.
* *
@@ -36,11 +36,6 @@ public:
void init(); void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots: public slots:
void selectProvider(); void selectProvider();
void selectModel(); 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. * This file is part of QodeAssist.
* *
@@ -23,42 +23,32 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QNetworkReply> #include <QNetworkReply>
#include <llmcore/RequestConfig.hpp>
#include <texteditor/textdocument.h>
#include "CodeHandler.hpp" #include "CodeHandler.hpp"
#include "context/DocumentContextReader.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 "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
namespace QodeAssist { namespace QodeAssist {
LLMClientInterface::LLMClientInterface( LLMClientInterface::LLMClientInterface()
const Settings::GeneralSettings &generalSettings, : m_requestHandler(this)
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))
{ {
} connect(&m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
LLMClientInterface::~LLMClientInterface() this,
{ &LLMClientInterface::sendCompletionToClient);
handleCancelRequest();
} }
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{ {
return "QodeAssist"; return "Qode Assist";
} }
void LLMClientInterface::startImpl() void LLMClientInterface::startImpl()
@@ -66,44 +56,6 @@ void LLMClientInterface::startImpl()
emit started(); 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));
// Send LSP error response to client
const RequestContext &ctx = it.value();
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::sendData(const QByteArray &data) void LLMClientInterface::sendData(const QByteArray &data)
{ {
QJsonDocument doc = QJsonDocument::fromJson(data); QJsonDocument doc = QJsonDocument::fromJson(data);
@@ -123,11 +75,10 @@ void LLMClientInterface::sendData(const QByteArray &data)
handleTextDocumentDidOpen(request); handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") { } else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.startTimeMeasurement(requestId); startTimeMeasurement(requestId);
handleCompletion(request); handleCompletion(request);
} else if (method == "cancelRequest") { } else if (method == "$/cancelRequest") {
qDebug() << "Cancelling request"; handleCancelRequest(request);
handleCancelRequest();
} else if (method == "exit") { } else if (method == "exit") {
// TODO make exit handler // TODO make exit handler
} else { } else {
@@ -135,29 +86,14 @@ void LLMClientInterface::sendData(const QByteArray &data)
} }
} }
void LLMClientInterface::handleCancelRequest() void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
{ {
QSet<LLMCore::Provider *> providers; QString id = request["params"].toObject()["id"].toString();
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) { if (m_requestHandler.cancelRequest(id)) {
if (it.value().provider) { LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
providers.insert(it.value().provider); } else {
LOG_MESSAGE(QString("Request %1 not found").arg(id));
} }
}
for (auto *provider : providers) {
disconnect(provider, nullptr, this, nullptr);
}
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();
LOG_MESSAGE("All requests cancelled and state cleared");
} }
void LLMClientInterface::handleInitialize(const QJsonObject &request) void LLMClientInterface::handleInitialize(const QJsonObject &request)
@@ -210,64 +146,46 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
emit finished(); emit finished();
} }
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage) bool QodeAssist::LLMClientInterface::isSpecifyCompletion(const QJsonObject &request)
{ {
QJsonObject response; auto &generalSettings = Settings::generalSettings();
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject errorObject; Context::ProgrammingLanguage documentLanguage = getDocumentLanguage(request);
errorObject["code"] = -32603; // Internal error code Context::ProgrammingLanguage preset1Language = Context::ProgrammingLanguageUtils::fromString(
errorObject["message"] = errorMessage; generalSettings.preset1Language.displayForIndex(generalSettings.preset1Language()));
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); return generalSettings.specifyPreset1() && documentLanguage == preset1Language;
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
} }
void LLMClientInterface::handleCompletion(const QJsonObject &request) void LLMClientInterface::handleCompletion(const QJsonObject &request)
{ {
auto filePath = Context::extractFilePathFromRequest(request); const auto updatedContext = prepareContext(request);
auto documentInfo = m_documentReader.readDocument(filePath); auto &completeSettings = Settings::codeCompletionSettings();
if (!documentInfo.document) { auto &generalSettings = Settings::generalSettings();
QString error = QString("Document is not available: %1").arg(filePath);
LOG_MESSAGE("Error: " + error);
sendErrorResponse(request, error);
return;
}
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() const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
: 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);
if (!provider) { if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName); LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return; return;
} }
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto templateName = !isPreset1Active ? generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template(); : generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName); auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
templateName);
if (!promptTemplate) { if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName); LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return; return;
} }
@@ -276,162 +194,107 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
config.requestType = LLMCore::RequestType::CodeCompletion; config.requestType = LLMCore::RequestType::CodeCompletion;
config.provider = provider; config.provider = provider;
config.promptTemplate = promptTemplate; config.promptTemplate = promptTemplate;
// TODO refactor networking config.url = QUrl(QString("%1%2").arg(
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) { url,
QString stream = QString{"streamGenerateContent?alt=sse"}; promptTemplate->type() == LLMCore::TemplateType::Fim ? provider->completionEndpoint()
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream)); : provider->chatEndpoint()));
} else {
config.url = QUrl(
QString("%1%2").arg(url, endpoint(provider, promptTemplate->type(), isPreset1Active)));
config.providerRequest = {{"model", modelName}, {"stream", true}};
}
config.apiKey = provider->apiKey(); 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()); const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
if (!stopWords.isEmpty()) if (!stopWords.isEmpty())
config.providerRequest["stop"] = stopWords; config.providerRequest["stop"] = stopWords;
QString systemPrompt; QString systemPrompt;
if (m_completeSettings.useSystemPrompt()) if (completeSettings.useSystemPrompt())
systemPrompt.append( systemPrompt.append(completeSettings.systemPrompt());
m_completeSettings.useUserMessageTemplateForCC() if (!updatedContext.fileContext.isEmpty())
&& promptTemplate->type() == LLMCore::TemplateType::Chat systemPrompt.append(updatedContext.fileContext);
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
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");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
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; QString userMessage;
if (m_completeSettings.useUserMessageTemplateForCC()) { if (completeSettings.useUserMessageTemplateForCC() && promptTemplate->type() == LLMCore::TemplateType::Chat) {
userMessage = m_completeSettings.processMessageToFIM( userMessage = completeSettings.userMessageTemplateForCC().arg(updatedContext.prefix, updatedContext.suffix);
updatedContext.prefix.value_or(""), updatedContext.suffix.value_or(""));
} else { } else {
userMessage = updatedContext.prefix.value_or("") + updatedContext.suffix.value_or(""); userMessage = updatedContext.prefix;
} }
// TODO refactor add message auto message = LLMCore::MessageBuilder()
QVector<LLMCore::Message> messages; .addSystemMessage(systemPrompt)
messages.append({"user", userMessage}); .addUserMessage(userMessage)
updatedContext.history = messages; .addSuffix(updatedContext.suffix)
} .addTokenizer(promptTemplate);
config.provider->prepareRequest( message.saveTo(
config.providerRequest, config.providerRequest,
promptTemplate, providerName == "Ollama" ? LLMCore::ProvidersApi::Ollama : LLMCore::ProvidersApi::OpenAI);
updatedContext,
LLMCore::RequestType::CodeCompletion, config.provider->prepareRequest(config.providerRequest, LLMCore::RequestType::CodeCompletion);
false,
false);
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type()); auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
QString error = QString("Request validation failed: %1").arg(errors.join("; ")); LOG_MESSAGE("Validate errors for fim request:");
LOG_MESSAGE("Validate errors for request:");
LOG_MESSAGES(errors); LOG_MESSAGES(errors);
sendErrorResponse(request, error);
return; return;
} }
m_requestHandler.sendLLMRequest(config, request);
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);
} }
LLMCore::ContextData LLMClientInterface::prepareContext( LLMCore::ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QJsonObject &request, const Context::DocumentInfo &documentInfo) const QStringView &accumulatedCompletion)
{ {
QJsonObject params = request["params"].toObject(); QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject(); QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].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 cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt(); int lineNumber = position["line"].toInt();
Context::DocumentContextReader Context::DocumentContextReader reader(textDocument);
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath); return reader.prepareContext(lineNumber, cursorPosition);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
} }
QString LLMClientInterface::endpoint( Context::ProgrammingLanguage LLMClientInterface::getDocumentLanguage(const QJsonObject &request) const
LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify)
{ {
QString endpoint; QJsonObject params = request["params"].toObject();
auto endpointMode = isLanguageSpecify ? m_generalSettings.ccPreset1EndpointMode.stringValue() QJsonObject doc = params["doc"].toObject();
: m_generalSettings.ccEndpointMode.stringValue(); QString uri = doc["uri"].toString();
if (endpointMode == "Auto") {
endpoint = type == LLMCore::TemplateType::FIM ? provider->completionEndpoint() Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
: provider->chatEndpoint(); TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
} else if (endpointMode == "Custom") { filePath);
endpoint = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint(); if (!textDocument) {
} else if (endpointMode == "FIM") { LOG_MESSAGE("Error: Document is not available for" + filePath.toString());
endpoint = provider->completionEndpoint(); return Context::ProgrammingLanguage::Unknown;
} else if (endpointMode == "Chat") {
endpoint = provider->chatEndpoint();
} }
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( auto templateName = !isPreset1Active ? Settings::generalSettings().ccTemplate()
const QString &completion, const QJsonObject &request, bool isComplete) : Settings::generalSettings().ccPreset1Template();
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate() auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
: m_generalSettings.ccPreset1Template(); templateName);
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject(); QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
@@ -443,38 +306,18 @@ void LLMClientInterface::sendCompletionToClient(
QJsonArray completions; QJsonArray completions;
QJsonObject completionItem; QJsonObject completionItem;
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion)); QString processedCompletion
= promptTemplate->type() == LLMCore::TemplateType::Chat
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue(); && Settings::codeCompletionSettings().smartProcessInstuctText()
QString processedCompletion; ? CodeHandler::processText(completion)
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; : completion;
}
if (processedCompletion.endsWith('\n')) {
QString withoutTrailing = processedCompletion.chopped(1);
if (!withoutTrailing.contains('\n')) {
LOG_MESSAGE(QString("Removed trailing newline from single-line completion"));
processedCompletion = withoutTrailing;
}
}
completionItem[LanguageServerProtocol::textKey] = processedCompletion; completionItem[LanguageServerProtocol::textKey] = processedCompletion;
QJsonObject range; QJsonObject range;
range["start"] = position; range["start"] = position;
range["end"] = position; QJsonObject end = position;
end["character"] = position["character"].toInt() + processedCompletion.length();
range["end"] = end;
completionItem[LanguageServerProtocol::rangeKey] = range; completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position; completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem); completions.append(completionItem);
@@ -486,13 +329,37 @@ void LLMClientInterface::sendCompletionToClient(
QString("Completions: \n%1") QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
LOG_MESSAGE( LOG_MESSAGE(QString("Full response: \n%1")
QString("Full response: \n%1")
.arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented)))); .arg(QString::fromUtf8(QJsonDocument(response).toJson(QJsonDocument::Indented))));
QString requestId = request["id"].toString(); QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId); endTimeMeasurement(requestId);
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response)); 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 } // 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. * This file is part of QodeAssist.
* *
@@ -22,15 +22,9 @@
#include <languageclient/languageclientinterface.h> #include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp> #include <context/ProgrammingLanguage.hpp>
#include <llmcore/ContextData.hpp> #include <llmcore/ContextData.hpp>
#include <llmcore/IPromptProvider.hpp> #include <llmcore/RequestHandler.hpp>
#include <llmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply; class QNetworkReply;
class QNetworkAccessManager; class QNetworkAccessManager;
@@ -42,33 +36,20 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT Q_OBJECT
public: public:
LLMClientInterface( LLMClientInterface();
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
LLMCore::IProviderRegistry &providerRegistry,
LLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
Utils::FilePath serverDeviceTemplate() const override; Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient( void sendCompletionToClient(const QString &completion,
const QString &completion, const QJsonObject &request, bool isComplete); const QJsonObject &request,
bool isComplete);
void handleCompletion(const QJsonObject &request); void handleCompletion(const QJsonObject &request);
// exposed for tests
void sendData(const QByteArray &data) override;
Context::ContextManager *contextManager() const;
protected: protected:
void startImpl() override; void startImpl() override;
void sendData(const QByteArray &data) override;
private slots: void parseCurrentMessage() override;
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
private: private:
void handleInitialize(const QJsonObject &request); void handleInitialize(const QJsonObject &request);
@@ -76,28 +57,20 @@ private:
void handleTextDocumentDidOpen(const QJsonObject &request); void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request); void handleInitialized(const QJsonObject &request);
void handleExit(const QJsonObject &request); void handleExit(const QJsonObject &request);
void handleCancelRequest(); void handleCancelRequest(const QJsonObject &request);
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
LLMCore::ContextData prepareContext( LLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo); const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
QString endpoint(LLMCore::Provider *provider, LLMCore::TemplateType type, bool isLanguageSpecify); Context::ProgrammingLanguage getDocumentLanguage(const QJsonObject &request) const;
bool isSpecifyCompletion(const QJsonObject &request);
const Settings::CodeCompletionSettings &m_completeSettings; LLMCore::RequestHandler m_requestHandler;
const Settings::GeneralSettings &m_generalSettings;
LLMCore::IPromptProvider *m_promptProvider = nullptr;
LLMCore::IProviderRegistry &m_providerRegistry;
Context::IDocumentReader &m_documentReader;
IRequestPerformanceLogger &m_performanceLogger;
QElapsedTimer m_completionTimer; QElapsedTimer m_completionTimer;
Context::ContextManager *m_contextManager; QMap<QString, qint64> m_requestStartTimes;
QHash<QString, RequestContext> m_activeRequests;
void startTimeMeasurement(const QString &requestId);
void endTimeMeasurement(const QString &requestId);
void logPerformance(const QString &requestId, const QString &operation, qint64 elapsedMs);
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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 QodeAssist.
* *
@@ -29,59 +29,6 @@
namespace QodeAssist { namespace QodeAssist {
static QStringList extractTokens(const QString &str)
{
QStringList tokens;
QString currentToken;
for (const QChar &ch : str) {
if (ch.isLetterOrNumber() || ch == '_') {
currentToken += ch;
} else {
if (!currentToken.isEmpty() && currentToken.length() > 1) {
tokens.append(currentToken);
}
currentToken.clear();
}
}
if (!currentToken.isEmpty() && currentToken.length() > 1) {
tokens.append(currentToken);
}
return tokens;
}
int LLMSuggestion::calculateReplaceLength(const QString &suggestion,
const QString &rightText,
const QString &entireLine)
{
if (rightText.isEmpty()) {
return 0;
}
QString structuralChars = "{}[]()<>;,";
bool hasStructuralOverlap = false;
for (const QChar &ch : structuralChars) {
if (suggestion.contains(ch) && rightText.contains(ch)) {
hasStructuralOverlap = true;
break;
}
}
if (hasStructuralOverlap) {
return rightText.length();
}
const QStringList suggestionTokens = extractTokens(suggestion);
const QStringList lineTokens = extractTokens(entireLine);
for (const auto &token : suggestionTokens) {
if (lineTokens.contains(token)) {
return rightText.length();
}
}
return 0;
}
LLMSuggestion::LLMSuggestion( LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion) const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion) : TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
@@ -89,38 +36,23 @@ LLMSuggestion::LLMSuggestion(
const auto &data = suggestions[currentCompletion]; const auto &data = suggestions[currentCompletion];
int startPos = data.range.begin.toPositionInDocument(sourceDocument); int startPos = data.range.begin.toPositionInDocument(sourceDocument);
int endPos = data.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount()); startPos = qBound(0, startPos, sourceDocument->characterCount() - 1);
endPos = qBound(startPos, endPos, sourceDocument->characterCount() - 1);
QTextCursor cursor(sourceDocument); QTextCursor cursor(sourceDocument);
cursor.setPosition(startPos); cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
QTextBlock block = cursor.block(); QTextBlock block = cursor.block();
QString blockText = block.text(); QString blockText = block.text();
int cursorPositionInBlock = cursor.positionInBlock(); int startPosInBlock = startPos - block.position();
QString leftText = blockText.left(cursorPositionInBlock); int endPosInBlock = endPos - block.position();
QString rightText = blockText.mid(cursorPositionInBlock);
QString suggestionText = data.text; blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
QString entireLine = blockText; replacementDocument()->setPlainText(blockText);
if (!suggestionText.contains('\n')) {
int replaceLength = calculateReplaceLength(suggestionText, rightText, entireLine);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + suggestionText + remainingRightText;
replacementDocument()->setPlainText(displayText);
} else {
int firstLineEnd = suggestionText.indexOf('\n');
QString firstLine = suggestionText.left(firstLineEnd);
QString restOfCompletion = suggestionText.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, rightText, entireLine);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
replacementDocument()->setPlainText(displayText);
}
} }
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
@@ -135,125 +67,41 @@ bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget) bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
{ {
const auto &currentSuggestions = suggestions(); const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
const auto &currentData = currentSuggestions[currentSuggestion()];
const Utils::Text::Range range = currentData.range;
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument()); const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
QTextCursor currentCursor = widget->textCursor(); QTextCursor currentCursor = widget->textCursor();
const QString text = currentData.text; const QString text = suggestions()[currentSuggestion()].text;
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock() const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
+ (cursor.selectionEnd() - cursor.selectionStart()); + (cursor.selectionEnd() - cursor.selectionStart());
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos); int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1) { if (next == -1)
if (part == Line) {
next = text.length();
} else {
return apply(); return apply();
}
}
if (part == Line) if (part == Line)
++next; ++next;
QString subText = text.mid(startPos, next - startPos); QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty())
if (subText.isEmpty()) {
return false; return false;
}
if (startPos == 0) {
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
if (replaceLength > 0) {
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
currentCursor.removeSelectedText();
}
}
if (!subText.contains('\n')) {
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());
const Utils::Text::Position
newEnd{newStart.line, newStart.column + int(remainingText.length())};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
} else {
currentCursor.insertText(subText); currentCursor.insertText(subText);
if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) { if (const int seperatorPos = subText.lastIndexOf('\n'); seperatorPos >= 0) {
const QString newCompletionText = text.mid(startPos + seperatorPos + 1); const QString newCompletionText = text.mid(startPos + seperatorPos + 1);
if (!newCompletionText.isEmpty()) { if (!newCompletionText.isEmpty()) {
const Utils::Text::Position newStart{int(range.begin.line + subText.count('\n')), 0}; 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::Position
newEnd{newStart.line, int(subText.length() - seperatorPos - 1)};
const Utils::Text::Range newRange{newStart, newEnd}; const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}}; const QList<Data> newSuggestion{{newRange, newEnd, newCompletionText}};
widget->insertSuggestion( widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0)); std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
} }
} }
}
return false; return false;
} }
bool LLMSuggestion::apply()
{
const auto &currentSuggestions = suggestions();
const auto &currentData = currentSuggestions[currentSuggestion()];
const Utils::Text::Range range = currentData.range;
const QTextCursor cursor = range.begin.toTextCursor(sourceDocument());
QString text = currentData.text;
QTextBlock currentBlock = cursor.block();
QString textBeforeCursor = currentBlock.text().left(cursor.positionInBlock());
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
QString entireLine = currentBlock.text();
QTextCursor editCursor = cursor;
editCursor.beginEditBlock();
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd != -1) {
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor, entireLine);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(firstLine + restOfText);
} else {
int replaceLength = calculateReplaceLength(text, textAfterCursor, entireLine);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
}
editCursor.endEditBlock();
return true;
}
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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 QodeAssist.
* *
@@ -40,10 +40,5 @@ public:
bool applyWord(TextEditor::TextEditorWidget *widget) override; bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override; bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget); bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
static int calculateReplaceLength(const QString &suggestion,
const QString &rightText,
const QString &entireLine);
}; };
} // namespace QodeAssist } // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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 QodeAssist.
* *
@@ -68,8 +68,7 @@ class GetCompletionParams : public LanguageServerProtocol::JsonObject
public: public:
static constexpr LanguageServerProtocol::Key docKey{"doc"}; static constexpr LanguageServerProtocol::Key docKey{"doc"};
GetCompletionParams( GetCompletionParams(const LanguageServerProtocol::TextDocumentIdentifier &document,
const LanguageServerProtocol::TextDocumentIdentifier &document,
int version, int version,
const LanguageServerProtocol::Position &position) const LanguageServerProtocol::Position &position)
{ {

View File

@@ -1,14 +1,13 @@
{ {
"Id" : "qodeassist", "Id" : "qodeassist",
"Name" : "QodeAssist", "Name" : "QodeAssist",
"Version" : "0.9.0", "Version" : "0.4.13",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev", "Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev", "VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd", "Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GPLv3", "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).", "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", "Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist", "DocumentationUrl" : "",
${IDE_PLUGIN_DEPENDENCIES} ${IDE_PLUGIN_DEPENDENCIES}
} }

View File

@@ -2,13 +2,5 @@
<qresource prefix="/"> <qresource prefix="/">
<file>resources/images/qoderassist-icon@2x.png</file> <file>resources/images/qoderassist-icon@2x.png</file>
<file>resources/images/qoderassist-icon.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> </qresource>
</RCC> </RCC>

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (C) 2023 The Qt Company Ltd. * 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: * The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0 * SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
@@ -24,24 +24,17 @@
#include "QodeAssistClient.hpp" #include "QodeAssistClient.hpp"
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer> #include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h> #include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h> #include <projectexplorer/projectmanager.h>
#include "LLMClientInterface.hpp" #include "LLMClientInterface.hpp"
#include "LLMSuggestion.hpp" #include "LLMSuggestion.hpp"
#include "RefactorSuggestion.hpp"
#include "RefactorSuggestionHoverHandler.hpp"
#include "settings/CodeCompletionSettings.hpp" #include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp" #include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp" #include "settings/ProjectSettings.hpp"
#include <context/ChangesManager.h> #include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol; using namespace LanguageServerProtocol;
using namespace TextEditor; using namespace TextEditor;
@@ -51,12 +44,11 @@ using namespace Core;
namespace QodeAssist { namespace QodeAssist {
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface) QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(clientInterface) : LanguageClient::Client(new LLMClientInterface())
, m_llmClient(clientInterface)
, m_recentCharCount(0) , m_recentCharCount(0)
{ {
setName("QodeAssist"); setName("Qode Assist");
LanguageClient::LanguageFilter filter; LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*"; filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter); setSupportedLanguage(filter);
@@ -65,14 +57,11 @@ QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
setupConnections(); setupConnections();
m_typingTimer.start(); m_typingTimer.start();
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
} }
QodeAssistClient::~QodeAssistClient() QodeAssistClient::~QodeAssistClient()
{ {
cleanupConnections(); cleanupConnections();
delete m_refactorHoverHandler;
} }
void QodeAssistClient::openDocument(TextEditor::TextDocument *document) void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
@@ -82,14 +71,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return; return;
Client::openDocument(document); Client::openDocument(document);
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
for (auto *editor : editors) {
if (auto *widget = editor->editorWidget()) {
widget->addHoverHandler(m_refactorHoverHandler);
widget->installEventFilter(this);
}
}
connect( connect(
document, document,
&TextDocument::contentsChangedWithPosition, &TextDocument::contentsChangedWithPosition,
@@ -147,13 +128,6 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
scheduleRequest(widget); 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) bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -168,32 +142,14 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project)) if (!isEnabled(project))
return; 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(); MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible()) if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return; return;
const FilePath filePath = editor->textDocument()->filePath(); const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{ GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath), documentVersion(filePath),
Position(cursor.mainCursor())}}; Position(cursor.mainCursor())}};
if (Settings::codeCompletionSettings().showProgressWidget()) {
// Setup cancel callback before showing progress
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
if (editor) {
cancelRunningRequest(editor);
}
});
m_progressHandler.showProgress(editor);
}
request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)]( request.setResponseCallback([this, editor = QPointer<TextEditorWidget>(editor)](
const GetCompletionRequest::Response &response) { const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return); QTC_ASSERT(editor, return);
@@ -203,42 +159,6 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request); 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);
}
// Setup cancel callback before showing progress
m_progressHandler.setCancelCallback([this, editor = QPointer<TextEditorWidget>(editor)]() {
if (editor && m_refactorHandler) {
m_refactorHandler->cancelRequest();
m_progressHandler.hideProgress();
}
});
m_progressHandler.showProgress(editor);
m_refactorHandler->sendRefactorRequest(editor, instructions);
}
void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor) void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
{ {
cancelRunningRequest(editor); cancelRunningRequest(editor);
@@ -268,19 +188,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position()); it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer()); it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
} }
void QodeAssistClient::handleCompletions( void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor) TextEditor::TextEditorWidget *editor)
{ {
m_progressHandler.hideProgress(); if (response.error())
if (response.error()) {
log(*response.error()); log(*response.error());
QString errorMessage = tr("Code completion failed: %1").arg(response.error()->message());
m_errorHandler.showError(editor, errorMessage);
return;
}
int requestPosition = -1; int requestPosition = -1;
if (const auto requestParams = m_runningRequests.take(editor).params()) if (const auto requestParams = m_runningRequests.take(editor).params())
requestPosition = requestParams->position().toPositionInDocument(editor->document()); requestPosition = requestParams->position().toPositionInDocument(editor->document());
@@ -323,11 +236,8 @@ void QodeAssistClient::handleCompletions(
Text::Position pos{toTextPos(c.position())}; Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()}; return TextSuggestion::Data{range, pos, c.text()};
}); });
if (completions.isEmpty())
if (completions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
return; return;
}
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document())); editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
} }
} }
@@ -337,7 +247,6 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor); const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd()) if (it == m_runningRequests.constEnd())
return; return;
m_progressHandler.hideProgress();
cancelRequest(it->id()); cancelRequest(it->id());
m_runningRequests.erase(it); m_runningRequests.erase(it);
} }
@@ -358,11 +267,16 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument); openDocument(textDocument);
}; };
m_documentOpenedConnection m_documentOpenedConnection = connect(EditorManager::instance(),
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc); &EditorManager::documentOpened,
m_documentClosedConnection = connect( this,
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) { openDoc);
if (auto textDocument = qobject_cast<TextDocument *>(document)) m_documentClosedConnection = connect(EditorManager::instance(),
&EditorManager::documentClosed,
this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument); closeDocument(textDocument);
}); });
@@ -379,118 +293,4 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear(); m_scheduledRequests.clear();
} }
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
m_progressHandler.hideProgress();
if (!result.success) {
// Show error to user
QString errorMessage = result.errorMessage.isEmpty()
? tr("Quick refactor failed")
: tr("Quick refactor failed: %1").arg(result.errorMessage);
if (result.editor) {
m_errorHandler.showError(result.editor, errorMessage);
}
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
if (!result.editor) {
LOG_MESSAGE("Refactoring result has no editor");
return;
}
TextEditorWidget *editorWidget = result.editor;
auto toTextPos = [](const Utils::Text::Position &pos) {
return Utils::Text::Position{pos.line, pos.column};
};
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
int startPos = range.begin.toPositionInDocument(editorWidget->document());
int endPos = range.end.toPositionInDocument(editorWidget->document());
if (startPos != endPos) {
QTextCursor startCursor(editorWidget->document());
startCursor.setPosition(startPos);
if (startCursor.positionInBlock() > 0) {
startCursor.movePosition(QTextCursor::StartOfBlock);
}
QTextCursor endCursor(editorWidget->document());
endCursor.setPosition(endPos);
if (endCursor.positionInBlock() > 0) {
endCursor.movePosition(QTextCursor::EndOfBlock);
if (!endCursor.atEnd()) {
endCursor.movePosition(QTextCursor::NextCharacter);
}
}
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
editorWidget->document(), startCursor.position());
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
editorWidget->document(), endCursor.position());
range = Utils::Text::Range(expandedBegin, expandedEnd);
}
TextEditor::TextSuggestion::Data suggestionData{
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
pos,
result.newText};
editorWidget->insertSuggestion(
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
m_refactorHoverHandler->setSuggestionRange(range);
m_refactorHoverHandler->setApplyCallback([this, editorWidget]() {
QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier);
QApplication::sendEvent(editorWidget, &tabEvent);
m_refactorHoverHandler->clearSuggestionRange();
});
m_refactorHoverHandler->setDismissCallback([this, editorWidget]() {
editorWidget->clearSuggestion();
m_refactorHoverHandler->clearSuggestionRange();
});
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
if (editor) {
if (m_runningRequests.contains(editor)) {
cancelRunningRequest(editor);
}
if (m_scheduledRequests.contains(editor)) {
auto *timer = m_scheduledRequests.value(editor);
if (timer && timer->isActive()) {
timer->stop();
}
}
if (m_refactorHandler && m_refactorHandler->isProcessing()) {
m_refactorHandler->cancelRequest();
}
m_progressHandler.hideProgress();
}
}
}
return LanguageClient::Client::eventFilter(watched, event);
}
} // namespace QodeAssist } // namespace QodeAssist

View File

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

View File

@@ -1,415 +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>
#include <settings/QuickRefactorSettings.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.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.qrTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::QuickRefactoring;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
config.apiKey = provider->apiKey();
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().qrUrl(),
Settings::generalSettings().qrModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
}
LLMCore::ContextData context = prepareContext(editor, range, instructions);
bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking();
provider->prepareRequest(
config.providerRequest,
promptTemplate,
context,
LLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
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();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
QString taggedContent;
bool readFullFile = Settings::quickRefactorSettings().readFullFile();
if (cursor.hasSelection()) {
int selStart = cursor.selectionStart();
int selEnd = cursor.selectionEnd();
QTextBlock startBlock = documentInfo.document->findBlock(selStart);
int startLine = startBlock.blockNumber();
int startColumn = selStart - startBlock.position();
QTextBlock endBlock = documentInfo.document->findBlock(selEnd);
int endLine = endBlock.blockNumber();
int endColumn = selEnd - endBlock.position();
QString contextBefore;
if (readFullFile) {
contextBefore = reader.readWholeFileBefore(startLine, startColumn);
} else {
contextBefore = reader.getContextBefore(
startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
}
QString selectedText = cursor.selectedText();
selectedText.replace(QChar(0x2029), "\n");
QString contextAfter;
if (readFullFile) {
contextAfter = reader.readWholeFileAfter(endLine, endColumn);
} else {
contextAfter = reader.getContextAfter(
endLine, endColumn, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
}
taggedContent = contextBefore;
if (selStart == cursorPos) {
taggedContent += "<cursor><selection_start>" + selectedText + "<selection_end>";
} else {
taggedContent += "<selection_start>" + selectedText + "<selection_end><cursor>";
}
taggedContent += contextAfter;
} else {
QTextBlock block = documentInfo.document->findBlock(cursorPos);
int line = block.blockNumber();
int column = cursorPos - block.position();
QString contextBefore;
if (readFullFile) {
contextBefore = reader.readWholeFileBefore(line, column);
} else {
contextBefore = reader.getContextBefore(
line, column, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
}
QString contextAfter;
if (readFullFile) {
contextAfter = reader.readWholeFileAfter(line, column);
} else {
contextAfter = reader.getContextAfter(
line, column, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
}
taggedContent = contextBefore + "<cursor>" + contextAfter;
}
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
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) {
m_isRefactoringInProgress = false;
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;
result.editor = m_currentEditor;
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) {
m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

View File

@@ -1,90 +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;
TextEditor::TextEditorWidget *editor{nullptr};
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
bool isProcessing() const { return m_isRefactoringInProgress; }
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

511
README.md
View File

@@ -2,9 +2,10 @@
[![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) [![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 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) ![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![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) [![](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 a comprehensive AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion, interactive chat with multiple interface options, inline quick refactoring, and AI function calling capabilities for C++ and QML development. Supporting both local providers (Ollama, llama.cpp, LM Studio) and cloud services (Claude, OpenAI, Google AI, Mistral AI), QodeAssist enhances your productivity with context-aware AI assistance, project-specific rules, and extensive customization options directly in your Qt development environment. ![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.
⚠️ **Important Notice About Paid Providers** ⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services: > When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@@ -12,43 +13,56 @@
> - The QodeAssist developer bears no responsibility for any charges incurred > - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use > - 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 ## Table of Contents
1. [Overview](#overview) 1. [Overview](#overview)
2. [Install Plugin](#install-plugin-to-qtcreator) 2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
3. [Configuration](#configuration) 3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
4. [Features](#features) 4. [Configure for OpenAI](#configure-for-openai)
5. [Context Layers](#context-layers) 5. [Configure for using Ollama](#configure-for-using-ollama)
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility) 6. [System Prompt Configuration](#system-prompt-configuration)
7. [Hotkeys](#hotkeys) 7. [File Context Features](#file-context-features)
8. [Troubleshooting](#troubleshooting) 8. [Template-Model Compatibility](#template-model-compatibility)
9. [Development Progress](#development-progress) 9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
10. [Support the Development](#support-the-development-of-qodeassist) 10. [Development Progress](#development-progress)
11. [How to Build](#how-to-build) 11. [Hotkeys](#hotkeys)
12. [Troubleshooting](#troubleshooting)
13. [Support the Development](#support-the-development-of-qodeassist)
14. [How to Build](#how-to-build)
## Overview ## Overview
QodeAssist enhances Qt Creator with AI-powered coding assistance: - AI-powered code completion
- Chat functionality:
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML - Side and Bottom panels
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel) - Chat history autosave and restore
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library - Token usage monitoring and management
- **File Context**: Attach or link files for better AI understanding - Attach files for one-time code analysis
- **Tool Calling**: AI can read project files, search code, and access diagnostics - Link files for persistent context with auto update in conversations
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more - Automatic syncing with open editor files (optional)
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates - Support for multiple LLM providers:
- Ollama
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users! - OpenAI
- Anthropic Claude
- LM Studio
- OpenAI-compatible providers(eg. https://openrouter.ai)
- Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection
<details> <details>
<summary>Code completion: (click to expand)</summary> <summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview"> <img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details> </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> <details>
<summary>Multiline Code completion: (click to expand)</summary> <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"> <img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@@ -64,58 +78,15 @@ QodeAssist enhances Qt Creator with AI-powered coding assistance:
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b"> <img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details> </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> <details>
<summary>Automatic syncing with open editor files: (click to expand)</summary> <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"> <img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details> </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 ## Install plugin to QtCreator
### Method 1: Using QodeAssistUpdater (Beta)
QodeAssistUpdater is a command-line utility that automates plugin installation and updates with automatic Qt Creator version detection and checksum verification.
**Features:**
- Automatic Qt Creator version detection
- Install, update, or remove plugin with single command
- List all available plugin versions
- Install specific plugin version
- Checksum verification
- Non-interactive mode for CI/CD
**Installation:**
Download pre-built binary from [QodeAssistUpdater releases](https://github.com/Palm1r/QodeAssistUpdater/releases) or build from source
**Usage:**
```bash
# Check current status and available updates
./qodeassist-updater --status
# Install latest version
./qodeassist-updater --install
```
For more information, visit the [QodeAssistUpdater repository](https://github.com/Palm1r/QodeAssistUpdater).
### Method 2: Manual Installation
1. Install Latest Qt Creator 1. Install Latest Qt Creator
2. Download the QodeAssist plugin for your Qt Creator 2. Download the QodeAssist plugin for your Qt Creator
- Remove old version plugin if already was installed - 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: 3. Launch Qt Creator and install the plugin:
- Go to: - Go to:
- MacOS: Qt Creator -> About Plugins... - MacOS: Qt Creator -> About Plugins...
@@ -123,242 +94,169 @@ For more information, visit the [QodeAssistUpdater repository](https://github.co
- Click on "Install Plugin..." - Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file - Select the downloaded QodeAssist plugin archive file
## Configuration ## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide: 2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
### Supported Providers - Set "Claude" as the provider for code completion or/and chat assistant
- Set the Claude URL (https://api.anthropic.com)
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider - Select your preferred model (e.g., claude-3-5-sonnet-20241022)
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server - Choose the Claude template for code completion or/and chat
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
### Additional Configuration
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features
### Code Completion
- AI-powered intelligent code completion
- Support for C++ and QML
- Context-aware suggestions
- Multiline completions
### Chat Assistant
- Multiple chat panels: side panel, bottom panel, and popup window
- Chat history with auto-save and restore
- Token usage monitoring
- **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional)
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
### Quick Refactoring
- Inline code refactoring directly in the editor with AI assistance
- Selection-based improvements with instant code replacement
- Built-in quick actions (repeat, improve, alternative)
- **Custom instructions library** with search and autocomplete
- Create, edit, and manage reusable refactoring templates
- Combine base instructions with specific details
- **[Learn more](docs/quick-refactoring.md)**
### Tools & Function Calling
- Read project files
- List and search in project
- Access linter/compiler issues
- Enabled by default (can be disabled)
## Context Layers
QodeAssist uses a flexible prompt composition system that adapts to different contexts. Here's how prompts are constructed for each feature:
<details> <details>
<summary><strong>Code Completion (FIM Models)</strong> - Codestral, Qwen2.5-Coder, DeepSeek-Coder (click to expand)</summary> <summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CODE COMPLETION (FIM Models) │
├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: Codestral, Qwen2.5-Coder, DeepSeek-Coder │
│ │
│ 1. System Prompt (from Code Completion Settings - FIM variant) │
│ 2. Project Rules: │
│ └─ .qodeassist/rules/completion/*.md │
│ 3. Open Files Context (optional, if enabled): │
│ └─ Currently open editor files │
│ 4. Code Context: │
│ ├─ Code before cursor (prefix) │
│ └─ Code after cursor (suffix) │
│ │
│ Final Prompt: FIM_Template(Prefix: SystemPrompt + Rules + OpenFiles + │
│ CodeBefore, │
│ Suffix: CodeAfter) │
└─────────────────────────────────────────────────────────────────────────────┘
```
</details> </details>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details> <details>
<summary><strong>Code Completion (Non-FIM Models)</strong> - DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct (click to expand)</summary> <summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CODE COMPLETION (Non-FIM/Chat Models) │
├─────────────────────────────────────────────────────────────────────────────┤
│ Examples: DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct │
│ │
│ 1. System Prompt (from Code Completion Settings - Non-FIM variant) │
│ └─ Includes response formatting instructions │
│ 2. Project Rules: │
│ └─ .qodeassist/rules/completion/*.md │
│ 3. Open Files Context (optional, if enabled): │
│ └─ Currently open editor files │
│ 4. Code Context: │
│ ├─ File information (language, path) │
│ ├─ Code before cursor │
│ ├─ <cursor> marker │
│ └─ Code after cursor │
│ 5. User Message: "Complete the code at cursor position" │
│ │
│ Final Prompt: [System: SystemPrompt + Rules] │
│ [User: OpenFiles + Context + CompletionRequest] │
└─────────────────────────────────────────────────────────────────────────────┘
```
</details> </details>
## 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:
For standard computers (minimum 8GB RAM):
```
ollama run qwen2.5-coder:7b
```
For better performance (16GB+ RAM):
```
ollama run qwen2.5-coder:14b
```
For high-end systems (32GB+ RAM):
```
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 "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
4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator.
<details> <details>
<summary><strong>Chat Assistant</strong> - Interactive coding assistant (click to expand)</summary> <summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings) │
│ 2. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/chat/*.md │
│ 3. File Context (optional): │
│ ├─ Attached files (manual) │
│ ├─ Linked files (persistent) │
│ └─ Open editor files (if auto-sync enabled) │
│ 4. Tool Definitions (if enabled): │
│ ├─ ReadProjectFileByName │
│ ├─ ListProjectFiles │
│ ├─ SearchInProject │
│ └─ GetIssuesList │
│ 5. Conversation History │
│ 6. User Message │
│ │
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
│ [History: Previous messages] │
│ [User: FileContext + UserMessage] │
└─────────────────────────────────────────────────────────────────────────────┘
```
</details> </details>
<details> ## System Prompt Configuration
<summary><strong>Quick Refactoring</strong> - Inline code improvements (click to expand)</summary>
``` 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.
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUICK REFACTORING │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Quick Refactor Settings) │
│ 2. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/quickrefactor/*.md │
│ 3. Code Context: │
│ ├─ File information (language, path) │
│ ├─ Code before selection (configurable amount) │
│ ├─ <selection_start> marker │
│ ├─ Selected code (or current line) │
│ ├─ <selection_end> marker │
│ ├─ <cursor> marker (position within selection) │
│ └─ Code after selection (configurable amount) │
│ 4. Refactor Instruction: │
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
│ ├─ Custom Instruction (from library) │
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
│ │ quick_refactor/instructions/*.json │
│ └─ Additional Details (optional user input) │
│ 5. Tool Definitions (if enabled) │
│ │
│ Final Prompt: [System: SystemPrompt + Rules] │
│ [User: Context + Markers + Instruction + Details] │
└─────────────────────────────────────────────────────────────────────────────┘
```
</details> ## File Context Features
### Key Points 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.
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure ### Attached Files
- **System Prompts** are configured independently for each feature in Settings
- **FIM vs Non-FIM models** for code completion use different System Prompts:
- FIM models: Direct completion prompt
- Non-FIM models: Prompt includes response formatting instructions
- **Quick Refactor** has its own provider/model configuration, independent from Chat
- **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details. Attachments are designed for one-time code analysis and specific queries:
- Files are included only in the current message
- Content is discarded after the message is processed
- Ideal for:
- Getting specific feedback on code changes
- Code review requests
- Analyzing isolated code segments
- Quick implementation questions
- Files can be attached using the paperclip icon in the chat interface
- Multiple files can be attached to a single message
### Linked Files
Linked files provide persistent context throughout the conversation:
- Files remain accessible for the entire chat session
- Content is included in every message exchange
- Files are automatically refreshed - always using latest content from disk
- Perfect for:
- Long-term refactoring discussions
- Complex architectural changes
- Multi-file implementations
- Maintaining context across related questions
- Can be managed using the link icon in the chat interface
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## 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 Version Compatibility
| Qt Creator Version | QodeAssist Version | - QtCreator 15.0.1 - 0.4.8 - 0.4.x
|-------------------|-------------------| - QtCreator 15.0.0 - 0.4.0 - 0.4.7
| 17.0.0+ | 0.6.0 - 0.x.x | - QtCreator 14.0.2 - 0.2.3 - 0.3.x
| 16.0.2 | 0.5.13 - 0.x.x | - QtCreator 14.0.1 - 0.2.2 plugin version and below
| 16.0.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 |
| 15.0.0 | 0.4.0 - 0.4.7 |
| 14.0.2 | 0.2.3 - 0.3.x |
| 14.0.1 | ≤ 0.2.2 |
## Hotkeys
All hotkeys can be customized in Qt Creator Settings. Default hotkeys:
| Action | macOS | Windows/Linux |
|--------|-------|--------------|
| Open chat window | ⌥⌘W | Ctrl+Alt+W |
| Close chat window | ⌥⌘S | Ctrl+Alt+S |
| Manual code suggestion | ⌥⌘Q | Ctrl+Alt+Q |
| Accept full suggestion | Tab | Tab |
| Accept word | ⌥→ | Alt+→ |
| Quick refactor | ⌥⌘R | Ctrl+Alt+R |
## Troubleshooting
Having issues with QodeAssist? Check our [detailed troubleshooting guide](docs/troubleshooting.md) for:
- Connection issues and provider URLs
- Model and template compatibility
- Platform-specific issues (Linux, macOS, Windows)
- Resetting settings to defaults
- Common problems and solutions
For additional support, join our [Discord Community](https://discord.gg/BGMkUsXUgf) or check [GitHub Issues](https://github.com/Palm1r/QodeAssist/issues).
## Development Progress ## Development Progress
- [x] Code completion functionality - [x] Basic plugin with code autocomplete functionality
- [x] Chat assistant with multiple panels - [x] Improve and automate settings
- [x] Diff sharing with models - [x] Add chat functionality
- [x] Tools/function calling support - [x] Sharing diff with model
- [x] Project-specific rules - [ ] Sharing project source with model
- [ ] Full project source sharing - [ ] Support for more providers and models
- [ ] Additional provider support
- [ ] MCP (Model Context Protocol) support ## Hotkeys
- To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q
- on Windows: 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
## Troubleshooting
If QodeAssist is having problems connecting to the LLM provider, please check the following:
1. Verify the IP address and port:
- For Ollama, the default is usually http://localhost:11434
- For LM Studio, the default is usually http://localhost:1234
2. Check the endpoint:
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. 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 "Qode Assist" tab
3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button
- The API key will not reset
- Select model after reset
## Support the development of QodeAssist ## Support the development of QodeAssist
If you find QodeAssist helpful, there are several ways you can support the project: If you find QodeAssist helpful, there are several ways you can support the project:
@@ -379,43 +277,20 @@ Every contribution, no matter how small, is greatly appreciated and helps keep t
## How to Build ## How to Build
### Prerequisites Create a build directory and run
- CMake 3.16+
- C++20 compatible compiler
- Qt Creator development files
### Build Steps cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
cmake --build .
1. Create a build directory: where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator build directory, or to a
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
```bash directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
mkdir build && cd build relative or absolute path to this plugin directory.
```
2. Configure and build:
```bash
cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
cmake --build .
```
**Path specifications:**
- `<path_to_qtcreator>`:
- **Windows/Linux**: Qt Creator build directory or combined binary package
- **macOS**: `Qt Creator.app/Contents/Resources/`
- `<path_to_plugin_source>`: Path to this plugin directory
## For Contributors ## For Contributors
### Code Style QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
- **C++**: Use `.clang-format` configuration in the project root
- Run formatting before submitting PRs
### Development Guidelines
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) ![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41) ![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

View File

@@ -1,202 +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 "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp"
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <texteditor/texteditor.h>
#include <logger/Logger.hpp>
namespace QodeAssist {
namespace {
QString extractLeadingWhitespace(const QString &text)
{
QString indent;
int firstLineEnd = text.indexOf('\n');
QString firstLine = (firstLineEnd != -1) ? text.left(firstLineEnd) : text;
for (int i = 0; i < firstLine.length(); ++i) {
if (firstLine[i].isSpace()) {
indent += firstLine[i];
} else {
break;
}
}
return indent;
}
} // anonymous namespace
RefactorSuggestion::RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument)
: TextEditor::TextSuggestion([&suggestion, sourceDocument]() {
Data expandedData = suggestion;
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(0, endPos, sourceDocument->characterCount());
if (startPos != endPos) {
QTextCursor startCursor(sourceDocument);
startCursor.setPosition(startPos);
int startPosInBlock = startCursor.positionInBlock();
if (startPosInBlock > 0) {
startCursor.movePosition(QTextCursor::StartOfBlock);
}
QTextCursor endCursor(sourceDocument);
endCursor.setPosition(endPos);
int endPosInBlock = endCursor.positionInBlock();
if (endPosInBlock > 0) {
endCursor.movePosition(QTextCursor::EndOfBlock);
if (!endCursor.atEnd()) {
endCursor.movePosition(QTextCursor::NextCharacter);
}
}
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
sourceDocument, startCursor.position());
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
sourceDocument, endCursor.position());
expandedData.range = Utils::Text::Range(expandedBegin, expandedEnd);
}
return expandedData;
}(), sourceDocument)
, m_suggestionData(suggestion)
{
const QString refactoredText = suggestion.text;
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(0, endPos, sourceDocument->characterCount());
QTextCursor startCursor(sourceDocument);
startCursor.setPosition(startPos);
if (startPos == endPos) {
QTextBlock block = startCursor.block();
QString blockText = block.text();
int startPosInBlock = startCursor.positionInBlock();
QString leftText = blockText.left(startPosInBlock);
QString rightText = blockText.mid(startPosInBlock);
QString displayText = leftText + refactoredText + rightText;
replacementDocument()->setPlainText(displayText);
} else {
QTextCursor fullLinesCursor(sourceDocument);
fullLinesCursor.setPosition(startPos);
fullLinesCursor.movePosition(QTextCursor::StartOfBlock);
int fullLinesStart = fullLinesCursor.position();
fullLinesCursor.setPosition(endPos);
fullLinesCursor.movePosition(QTextCursor::EndOfBlock);
int fullLinesEnd = fullLinesCursor.position();
fullLinesCursor.setPosition(fullLinesStart);
fullLinesCursor.setPosition(fullLinesEnd, QTextCursor::KeepAnchor);
QString fullLinesText = fullLinesCursor.selectedText();
fullLinesText.replace(QChar(0x2029), "\n");
QString oldIndent = extractLeadingWhitespace(fullLinesText);
QString newIndent = extractLeadingWhitespace(refactoredText);
QString displayText = refactoredText;
if (newIndent.length() < oldIndent.length()) {
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
QStringList lines = refactoredText.split('\n');
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
lines[0] = indentDiff + lines[0];
displayText = lines.join('\n');
}
}
replacementDocument()->setPlainText(displayText);
}
}
bool RefactorSuggestion::apply()
{
const QString text = m_suggestionData.text;
const Utils::Text::Range range = m_suggestionData.range;
const QTextCursor startCursor = range.begin.toTextCursor(sourceDocument());
const QTextCursor endCursor = range.end.toTextCursor(sourceDocument());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(sourceDocument());
editCursor.beginEditBlock();
if (startPos == endPos) {
editCursor.setPosition(startPos);
editCursor.insertText(text);
} else {
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
QString selectedText = editCursor.selectedText();
selectedText.replace(QChar(0x2029), "\n");
QString oldIndent = extractLeadingWhitespace(selectedText);
QString newIndent = extractLeadingWhitespace(text);
QString textToInsert = text;
if (newIndent.length() < oldIndent.length()) {
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
QStringList lines = text.split('\n');
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
lines[0] = indentDiff + lines[0];
textToInsert = lines.join('\n');
}
}
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
editCursor.insertText(textToInsert);
}
editCursor.endEditBlock();
return true;
}
bool RefactorSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{
Q_UNUSED(widget)
return apply();
}
bool RefactorSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{
Q_UNUSED(widget)
return apply();
}
} // namespace QodeAssist

View File

@@ -1,43 +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 <texteditor/texteditor.h>
#include <texteditor/textsuggestion.h>
namespace QodeAssist {
class RefactorSuggestion : public TextEditor::TextSuggestion
{
public:
RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument);
bool apply() override;
bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override;
private:
Data m_suggestionData;
};
} // namespace QodeAssist

View File

@@ -1,210 +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 "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp"
#include <QColor>
#include <QHBoxLayout>
#include <QPushButton>
#include <QScopeGuard>
#include <QTextBlock>
#include <QTextCursor>
#include <QWidget>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
#include <utils/theme/theme.h>
#include <utils/tooltip/tooltip.h>
#include <logger/Logger.hpp>
namespace QodeAssist {
RefactorSuggestionHoverHandler::RefactorSuggestionHoverHandler()
{
setPriority(Priority_Suggestion);
}
void RefactorSuggestionHoverHandler::setSuggestionRange(const Utils::Text::Range &range)
{
m_suggestionRange = range;
m_hasSuggestion = true;
}
void RefactorSuggestionHoverHandler::clearSuggestionRange()
{
m_hasSuggestion = false;
}
void RefactorSuggestionHoverHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget,
int pos,
ReportPriority report)
{
QScopeGuard cleanup([&] { report(Priority_None); });
if (!editorWidget->suggestionVisible()) {
return;
}
QTextCursor cursor(editorWidget->document());
cursor.setPosition(pos);
m_block = cursor.block();
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
TextEditor::TextBlockUserData::suggestion(m_block));
#else
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
if (!userData) {
LOG_MESSAGE("RefactorSuggestionHoverHandler: No user data in block");
return;
}
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
#endif
if (!suggestion) {
return;
}
cleanup.dismiss();
report(Priority_Suggestion);
}
void RefactorSuggestionHoverHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget,
const QPoint &point)
{
Q_UNUSED(point)
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
TextEditor::TextBlockUserData::suggestion(m_block));
#else
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
if (!userData) {
LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: No user data in block");
return;
}
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
#endif
if (!suggestion) {
return;
}
auto *widget = new QWidget();
auto *layout = new QHBoxLayout(widget);
layout->setContentsMargins(4, 3, 4, 3);
layout->setSpacing(6);
const QColor normalBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
const QColor hoverBg = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
const QColor selectedBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
const QColor textColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
const QColor successColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
const QColor errorColor = Utils::creatorColor(Utils::Theme::TextColorError);
auto *applyButton = new QPushButton("✓ Apply", widget);
applyButton->setFocusPolicy(Qt::NoFocus);
applyButton->setToolTip("Apply refactoring (Tab)");
applyButton->setCursor(Qt::PointingHandCursor);
applyButton->setStyleSheet(QString(
"QPushButton {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" border-radius: 3px;"
" padding: 4px 12px;"
" font-weight: bold;"
" font-size: 11px;"
" min-width: 60px;"
"}"
"QPushButton:hover {"
" background-color: %4;"
" border-color: %2;"
"}"
"QPushButton:pressed {"
" background-color: %5;"
"}")
.arg(selectedBg.name())
.arg(successColor.name())
.arg(borderColor.name())
.arg(selectedBg.lighter(110).name())
.arg(selectedBg.darker(110).name()));
QObject::connect(applyButton, &QPushButton::clicked, widget, [this]() {
Utils::ToolTip::hide();
if (m_applyCallback) {
m_applyCallback();
}
});
auto *dismissButton = new QPushButton("✕ Dismiss", widget);
dismissButton->setFocusPolicy(Qt::NoFocus);
dismissButton->setToolTip("Dismiss refactoring (Esc)");
dismissButton->setCursor(Qt::PointingHandCursor);
dismissButton->setStyleSheet(QString(
"QPushButton {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" border-radius: 3px;"
" padding: 4px 12px;"
" font-size: 11px;"
" min-width: 60px;"
"}"
"QPushButton:hover {"
" background-color: %4;"
" color: %5;"
" border-color: %5;"
"}"
"QPushButton:pressed {"
" background-color: %6;"
"}")
.arg(normalBg.name())
.arg(textColor.name())
.arg(borderColor.name())
.arg(hoverBg.name())
.arg(errorColor.name())
.arg(hoverBg.darker(110).name()));
QObject::connect(dismissButton, &QPushButton::clicked, widget, [this]() {
Utils::ToolTip::hide();
if (m_dismissCallback) {
m_dismissCallback();
}
});
layout->addWidget(applyButton);
layout->addWidget(dismissButton);
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
pos.ry() -= widget->sizeHint().height();
Utils::ToolTip::show(pos, widget, editorWidget);
}
} // namespace QodeAssist

View File

@@ -1,72 +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 <functional>
#include <QTextBlock>
#include <texteditor/basehoverhandler.h>
#include <utils/textutils.h>
namespace TextEditor {
class TextEditorWidget;
}
namespace QodeAssist {
/**
* @brief Hover handler for refactoring suggestions
*
* Shows interactive tooltip with Apply/Dismiss buttons when hovering over
* a refactoring suggestion in the editor.
*/
class RefactorSuggestionHoverHandler : public TextEditor::BaseHoverHandler
{
public:
using ApplyCallback = std::function<void()>;
using DismissCallback = std::function<void()>;
RefactorSuggestionHoverHandler();
void setSuggestionRange(const Utils::Text::Range &range);
void clearSuggestionRange();
bool hasSuggestion() const { return m_hasSuggestion; }
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
protected:
void identifyMatch(TextEditor::TextEditorWidget *editorWidget,
int pos,
ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget,
const QPoint &point) override;
private:
Utils::Text::Range m_suggestionRange;
bool m_hasSuggestion = false;
ApplyCallback m_applyCallback;
DismissCallback m_dismissCallback;
QTextBlock m_block;
};
} // namespace QodeAssist

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

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