Compare commits
335 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a8fbe1792 | ||
|
|
d867a6f0be | ||
|
|
248530c746 | ||
|
|
c73b71f328 | ||
|
|
d2c1e39a2e | ||
|
|
e86e7e103e | ||
|
|
42199024ff | ||
|
|
620fded2e1 | ||
|
|
90b7ed26b1 | ||
|
|
25c4d5f185 | ||
|
|
7a551ed384 | ||
|
|
ca0a47b160 | ||
|
|
6b069b55e3 | ||
|
|
2891b313d2 | ||
|
|
ede2c01eb7 | ||
|
|
6c05f0d594 | ||
|
|
15d714588f | ||
|
|
9a2ba08538 | ||
|
|
37084bec59 | ||
|
|
6910037e97 | ||
|
|
a72cdd85a4 | ||
|
|
31b4e73af5 | ||
|
|
088887c802 | ||
|
|
b7a9787cc3 | ||
|
|
e2e13f0f38 | ||
|
|
49ae335d7d | ||
|
|
2ba58a403f | ||
|
|
3de1619bf0 | ||
|
|
ec45067336 | ||
|
|
52fb65c5b1 | ||
|
|
478f369ad2 | ||
|
|
762c965377 | ||
|
|
d2b93310e2 | ||
|
|
f3b1e7f411 | ||
|
|
a55c6ccfdb | ||
|
|
b32433c336 | ||
|
|
6f11260cd1 | ||
|
|
ddd6aba091 | ||
|
|
e3f464c54e | ||
|
|
e86e58337a | ||
|
|
dbd47387be | ||
|
|
50e1276ab2 | ||
|
|
50c948ccfe | ||
|
|
949dad4fd2 | ||
|
|
01fd7dad6f | ||
|
|
fd408ba415 | ||
|
|
14e7ea2ec3 | ||
|
|
9f050aec67 | ||
|
|
9e118ddfaf | ||
|
|
157498b770 | ||
|
|
5c8a8f305d | ||
|
|
fc33bb60d0 | ||
|
|
498eb4d932 | ||
|
|
fb941cea99 | ||
|
|
a0af983bda | ||
|
|
4bd96e0718 | ||
|
|
7b0d3c2abb | ||
|
|
75d1551b00 | ||
|
|
406ba05bfb | ||
|
|
7a97d0aba5 | ||
|
|
b19c4c0c0c | ||
|
|
a466332822 | ||
|
|
e1fa01d123 | ||
|
|
37e41d3b76 | ||
|
|
2d5667d8ca | ||
|
|
22377c8f6a | ||
|
|
595895840a | ||
|
|
f6d647d5c8 | ||
|
|
1f9c60ffb2 | ||
|
|
6ec4a61c0c | ||
|
|
7feb088de3 | ||
|
|
627a821115 | ||
|
|
9b0ae98f02 | ||
|
|
85a7bba90e | ||
|
|
b18ef4c400 | ||
|
|
bbacdfc22a | ||
|
|
670f81c3dd | ||
|
|
b4f31dee23 | ||
|
|
dc6ec4fb4f | ||
|
|
07de415346 | ||
|
|
a15f64a234 | ||
|
|
0feaa3a0f7 | ||
|
|
a3527e1442 | ||
|
|
24565dc81f | ||
|
|
90655cded4 | ||
|
|
1e3b1997cc | ||
|
|
6f7d8a0987 | ||
|
|
55b6080273 | ||
|
|
ce9e2717d6 | ||
|
|
ef73895823 | ||
|
|
bcdec96d92 | ||
|
|
86c6930c5f | ||
|
|
f3aa706227 | ||
|
|
944e7fb00a | ||
|
|
296a0ff7b8 | ||
|
|
06bd7db7ea | ||
|
|
43f9e4e75b | ||
|
|
204cffd7d0 | ||
|
|
995597d789 | ||
|
|
9974b2f5e6 | ||
|
|
31e3d9db7c | ||
|
|
6f680e3974 | ||
|
|
953774aaa8 | ||
|
|
9ecd285d1d | ||
|
|
0ca1decd97 | ||
|
|
baf129f0dc | ||
|
|
8570b9667a | ||
|
|
f5a445b021 | ||
|
|
30885c0373 | ||
|
|
5e580b8792 | ||
|
|
5352fd4f0b | ||
|
|
6e9db1552c | ||
|
|
c302138568 | ||
|
|
75cbc46808 | ||
|
|
1070de6e6e | ||
|
|
b3432cd76f | ||
|
|
f99e4aefb0 | ||
|
|
191de10926 | ||
|
|
8492ef29b2 | ||
|
|
fea9ecddc8 | ||
|
|
a26d475806 | ||
|
|
1cd19aa5d1 | ||
|
|
4956d6ab7d | ||
|
|
2d92b8fa53 | ||
|
|
161d77ac04 | ||
|
|
89797639cf | ||
|
|
4d79760481 | ||
|
|
3be70556ec | ||
|
|
a1ff17eef0 | ||
|
|
6c1d9ddc0e | ||
|
|
6ff6901421 | ||
|
|
a2f3ae4f64 | ||
|
|
6937b48fbf | ||
|
|
c6e77c59d3 | ||
|
|
655471aec6 | ||
|
|
af90d3cad2 | ||
|
|
9b90aaa06e | ||
|
|
e7110810f8 | ||
|
|
1848d44503 | ||
|
|
db82fb08e8 | ||
|
|
9117572f82 | ||
|
|
a143cc8e20 | ||
|
|
eae2b748d5 | ||
|
|
64bca47290 | ||
|
|
531fce96b5 | ||
|
|
e7e437590a | ||
|
|
00b7287e08 | ||
|
|
5a49a2e7eb | ||
|
|
3b56c1f07a | ||
|
|
d483ca372d | ||
|
|
dfd9979450 | ||
|
|
6cb0b14b18 | ||
|
|
43b64b9166 | ||
|
|
608103b92e | ||
|
|
e1025df21e | ||
|
|
fad2453dbe | ||
|
|
5e1530715c | ||
|
|
dfac209c23 | ||
|
|
cab8718979 | ||
|
|
5dc28fc1ad | ||
|
|
1122332423 | ||
|
|
7e878cdbf8 | ||
|
|
c95b20d6d4 | ||
|
|
70c610997a | ||
|
|
8a4bf54fff | ||
|
|
db7da29fa4 | ||
|
|
a0a76f2665 | ||
|
|
56354e8d87 | ||
|
|
b7322be00c | ||
|
|
254fac246d | ||
|
|
0365018834 | ||
|
|
755be518be | ||
|
|
fe82b48bef | ||
|
|
8a338ecb69 | ||
|
|
238ca00227 | ||
|
|
18fb2b530f | ||
|
|
f0d2e42680 | ||
|
|
ff0f994ec6 | ||
|
|
45df27e749 | ||
|
|
02863003a9 | ||
|
|
002b8e01e5 | ||
|
|
f54d1185aa | ||
|
|
bcb0c6f761 | ||
|
|
d285ab6117 | ||
|
|
5ae6f9e3bf | ||
|
|
fb5903e44f | ||
|
|
ce66c8e4f7 | ||
|
|
5f094887e7 | ||
|
|
69d9af1a97 | ||
|
|
86b52bf858 | ||
|
|
cac6068ee7 | ||
|
|
8d495dd1bf | ||
|
|
906c161729 | ||
|
|
ebd71daf3d | ||
|
|
84770abb20 | ||
|
|
b4e8bdf6da | ||
|
|
d2b28093a6 | ||
|
|
bde58fb9aa | ||
|
|
d4b6f8976b | ||
|
|
cd08b5d919 | ||
|
|
f6de03f601 | ||
|
|
1a08eebe92 | ||
|
|
ea4f8b9df9 | ||
|
|
7f77f7175d | ||
|
|
bed42f9098 | ||
|
|
10b924d78a | ||
|
|
8aa37c5c8c | ||
|
|
f8b87da2ca | ||
|
|
7663bd34af | ||
|
|
a52c86c6f0 | ||
|
|
ac53296e85 | ||
|
|
c688cba3dd | ||
|
|
ff750c271a | ||
|
|
d0f8c1098f | ||
|
|
5cde6ffac7 | ||
|
|
8c6f1e514b | ||
|
|
99cd79aac8 | ||
|
|
d2b6c11569 | ||
|
|
ec1b5bdf5f | ||
|
|
561661b476 | ||
|
|
76309be0a6 | ||
|
|
5969d530bd | ||
|
|
809f1c6614 | ||
|
|
851e681cf5 | ||
|
|
f2f3b7cce0 | ||
|
|
5b7a9b681c | ||
|
|
29af277139 | ||
|
|
a45786bd00 | ||
|
|
695b35b510 | ||
|
|
5a23ab9c5a | ||
|
|
c36dffea93 | ||
|
|
4b7eed2779 | ||
|
|
88c11c4702 | ||
|
|
543c79161d | ||
|
|
aa2edf5954 | ||
|
|
894fec860a | ||
|
|
e4324f8e80 | ||
|
|
6a0198ae9b | ||
|
|
e136d6056a | ||
|
|
ff027b12af | ||
|
|
0bdf77f38d | ||
|
|
21814e8809 | ||
|
|
d732e2f9aa | ||
|
|
bf6d09a068 | ||
|
|
c3f2011c29 | ||
|
|
af3fdb58ff | ||
|
|
637a4d9d4c | ||
|
|
7e2345773f | ||
|
|
14a5ddbdd8 | ||
|
|
e178b7daa7 | ||
|
|
4b353d5091 | ||
|
|
f7ba7b95be | ||
|
|
6ae95fec45 | ||
|
|
dad8ab2bf3 | ||
|
|
25a6983de0 | ||
|
|
4e05abc7d2 | ||
|
|
784529e344 | ||
|
|
155153a763 | ||
|
|
9225c0c1a9 | ||
|
|
43adc95857 | ||
|
|
ee672f2cda | ||
|
|
a3edb8a577 | ||
|
|
407d3b11c0 | ||
|
|
285e739074 | ||
|
|
f7e748ba7e | ||
|
|
acb1306321 | ||
|
|
8b38ecc29b | ||
|
|
cfb364f033 | ||
|
|
2fe6850a06 | ||
|
|
3e9506ca92 | ||
|
|
d24adff0f5 | ||
|
|
447324eb07 | ||
|
|
4ca494cc51 | ||
|
|
8a80dbe8f5 | ||
|
|
2b539bbdeb | ||
|
|
3f2c146df1 | ||
|
|
9a54f04a0d | ||
|
|
7a33425d1a | ||
|
|
711aa672f2 | ||
|
|
8cb6a2f6d2 | ||
|
|
2f9622e23e | ||
|
|
674b1fecde | ||
|
|
b36d01d2c7 | ||
|
|
615175bea8 | ||
|
|
7515599acb | ||
|
|
3652d4d5d9 | ||
|
|
75677770b2 | ||
|
|
329a1efd5d | ||
|
|
27760a3b99 | ||
|
|
a93b3cd7f5 | ||
|
|
bacde51d71 | ||
|
|
418578743a | ||
|
|
56e5ef22f1 | ||
|
|
e90933d713 | ||
|
|
5b9c67c2d8 | ||
|
|
fe84a2a303 | ||
|
|
62de53c306 | ||
|
|
7c6a10936c | ||
|
|
032c9bbbf3 | ||
|
|
8906f98038 | ||
|
|
5126092449 | ||
|
|
9d2d70fc63 | ||
|
|
ffaf6bd61b | ||
|
|
79218d8412 | ||
|
|
7e6e526ac8 | ||
|
|
80646e2af0 | ||
|
|
5808a892c1 | ||
|
|
d58ff90458 | ||
|
|
7d06ab04dc | ||
|
|
9d40e8ca25 | ||
|
|
5b16c5403a | ||
|
|
4ddbe0b8b9 | ||
|
|
f41e063c02 | ||
|
|
9d7d084448 | ||
|
|
1ca1ffc629 | ||
|
|
8419577ae5 | ||
|
|
91a6a88130 | ||
|
|
be38abc505 | ||
|
|
f2e0afb6b8 | ||
|
|
3cf07238fd | ||
|
|
b98f85a997 | ||
|
|
085659483f | ||
|
|
8a1fd5438e | ||
|
|
78f69e82a5 | ||
|
|
3d770f91c7 | ||
|
|
c724bace06 | ||
|
|
719065ebfc | ||
|
|
a218064a4f | ||
|
|
13cd12b00a | ||
|
|
ed59be4199 | ||
|
|
7dd8b3d085 | ||
|
|
3839d6896c | ||
|
|
6b86637dcb | ||
|
|
58c3e26e7f | ||
|
|
98e1047bf1 |
2
.github/FUNDING.yml
vendored
@@ -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: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: ['https://www.paypal.com/paypalme/palm1r', 'https://github.com/Palm1r/QodeAssist#support-the-development-of-qodeassist']
|
||||||
|
|||||||
61
.github/scripts/plugin.json
vendored
@@ -6,22 +6,77 @@
|
|||||||
"llm",
|
"llm",
|
||||||
"ai"
|
"ai"
|
||||||
],
|
],
|
||||||
"compatibility": "Qt 6.8.1",
|
"compatibility": "Qt 6.8.3",
|
||||||
"platforms": [
|
"platforms": [
|
||||||
"Windows",
|
"Windows",
|
||||||
"macOS",
|
"macOS",
|
||||||
"Linux"
|
"Linux"
|
||||||
],
|
],
|
||||||
"license": "GPLv3",
|
"license": "GPLv3",
|
||||||
"version": "0.4.0",
|
"version": "0.5.11",
|
||||||
"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": true,
|
"is_latest": false,
|
||||||
"released_at": "2024-01-24T15:00:00Z"
|
"released_at": "2024-01-24T15:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.2",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-03-13T17:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.3",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-03-14T11:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.4",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-03-17T03:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.5",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-03-20T19:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.6",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-04T19:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.7",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-14T01:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.8",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-17T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.9",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-21T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.10",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-24T10:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.11",
|
||||||
|
"is_latest": false,
|
||||||
|
"released_at": "2025-04-24T21:00:00Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "0.5.12",
|
||||||
|
"is_latest": true,
|
||||||
|
"released_at": "2025-05-01T17:00:00Z"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
|
||||||
|
|||||||
140
.github/workflows/build_cmake.yml
vendored
@@ -12,16 +12,13 @@ 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 }}
|
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
|
||||||
runs-on: ${{ matrix.config.os }}
|
runs-on: ${{ matrix.config.os }}
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.git.outputs.tag }}
|
tag: ${{ steps.git.outputs.tag }}
|
||||||
@@ -47,12 +44,20 @@ jobs:
|
|||||||
platform: mac_x64,
|
platform: mac_x64,
|
||||||
cc: "clang", cxx: "clang++"
|
cc: "clang", cxx: "clang++"
|
||||||
}
|
}
|
||||||
|
qt_config:
|
||||||
|
- {
|
||||||
|
qt_version: "6.10.1",
|
||||||
|
qt_creator_version: "18.0.2"
|
||||||
|
}
|
||||||
|
- {
|
||||||
|
qt_version: "6.10.2",
|
||||||
|
qt_creator_version: "19.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
submodules: recursive
|
||||||
|
|
||||||
- name: Checkout submodules
|
- name: Checkout submodules
|
||||||
id: git
|
id: git
|
||||||
@@ -61,11 +66,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()
|
||||||
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}")
|
execute_process(
|
||||||
|
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@latest
|
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
|
||||||
with:
|
with:
|
||||||
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
cmakeVersion: ${{ env.CMAKE_VERSION }}
|
||||||
ninjaVersion: ${{ env.NINJA_VERSION }}
|
ninjaVersion: ${{ env.NINJA_VERSION }}
|
||||||
@@ -80,7 +90,7 @@ jobs:
|
|||||||
execute_process(
|
execute_process(
|
||||||
COMMAND sudo apt install
|
COMMAND sudo apt install
|
||||||
# build dependencies
|
# build dependencies
|
||||||
libgl1-mesa-dev libgtest-dev
|
libgl1-mesa-dev libgtest-dev libgmock-dev
|
||||||
# runtime dependencies for tests (Qt is downloaded outside package manager,
|
# runtime dependencies for tests (Qt is downloaded outside package manager,
|
||||||
# thus minimal dependencies must be installed explicitly)
|
# thus minimal dependencies must be installed explicitly)
|
||||||
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
|
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
|
||||||
@@ -96,14 +106,19 @@ jobs:
|
|||||||
id: qt
|
id: qt
|
||||||
shell: cmake -P {0}
|
shell: cmake -P {0}
|
||||||
run: |
|
run: |
|
||||||
set(qt_version "$ENV{QT_VERSION}")
|
set(qt_version "${{ matrix.qt_config.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")
|
||||||
@@ -112,12 +127,20 @@ 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}")
|
||||||
@@ -140,7 +163,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)
|
foreach(package qtbase qtdeclarative qttools qtsvg)
|
||||||
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
|
||||||
@@ -174,10 +197,11 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Download Qt Creator
|
- name: Download Qt Creator
|
||||||
uses: qt-creator/install-dev-package@v1.2
|
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
|
||||||
with:
|
with:
|
||||||
version: ${{ env.QT_CREATOR_VERSION }}
|
version: ${{ matrix.qt_config.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
|
||||||
@@ -193,15 +217,6 @@ jobs:
|
|||||||
set(ENV{CXX} ${{ matrix.config.cxx }})
|
set(ENV{CXX} ${{ matrix.config.cxx }})
|
||||||
set(ENV{MACOSX_DEPLOYMENT_TARGET} "${{ env.MACOS_DEPLOYMENT_TARGET }}")
|
set(ENV{MACOSX_DEPLOYMENT_TARGET} "${{ env.MACOS_DEPLOYMENT_TARGET }}")
|
||||||
|
|
||||||
string(REGEX MATCH "^([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match "$ENV{QT_CREATOR_VERSION}")
|
|
||||||
set(QT_CREATOR_VERSION_MAJOR "${CMAKE_MATCH_1}")
|
|
||||||
set(QT_CREATOR_VERSION_MINOR "${CMAKE_MATCH_2}")
|
|
||||||
set(QT_CREATOR_VERSION_PATCH "${CMAKE_MATCH_3}")
|
|
||||||
|
|
||||||
if(NOT version_match)
|
|
||||||
message(FATAL_ERROR "Failed to parse Qt Creator version string: $ENV{QT_CREATOR_VERSION}")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")
|
if ("${{ runner.os }}" STREQUAL "Windows" AND NOT "x${{ matrix.config.environment_script }}" STREQUAL "x")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${{ matrix.config.environment_script }}" && set
|
COMMAND "${{ matrix.config.environment_script }}" && set
|
||||||
@@ -232,15 +247,12 @@ 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}-$ENV{QT_CREATOR_VERSION}-${{ matrix.config.artifact }}"
|
--name "$ENV{PLUGIN_NAME}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.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 }}"
|
||||||
--qtc-path "${{ steps.qt_creator.outputs.qtc_dir }}"
|
--qtc-path "${{ steps.qt_creator.outputs.qtc_dir }}"
|
||||||
--output-path "$ENV{GITHUB_WORKSPACE}"
|
--output-path "$ENV{GITHUB_WORKSPACE}"
|
||||||
--add-config=-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QT_CREATOR_VERSION_MAJOR}
|
|
||||||
--add-config=-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QT_CREATOR_VERSION_MINOR}
|
|
||||||
--add-config=-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QT_CREATOR_VERSION_PATCH}
|
|
||||||
RESULT_VARIABLE result
|
RESULT_VARIABLE result
|
||||||
)
|
)
|
||||||
if (NOT result EQUAL 0)
|
if (NOT result EQUAL 0)
|
||||||
@@ -251,72 +263,24 @@ jobs:
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||||
with:
|
with:
|
||||||
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
|
path: ./${{ 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: ${{ env.PLUGIN_NAME}}-v${{ steps.git.outputs.tag }}-QtC${{ matrix.qt_config.qt_creator_version }}-${{ matrix.config.artifact }}.7z
|
||||||
|
|
||||||
# The json is the same for all platforms, but we need to save one
|
|
||||||
- name: Upload plugin json
|
|
||||||
if: startsWith(matrix.config.os, 'ubuntu')
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: ${{ env.PLUGIN_NAME }}-origin-json
|
|
||||||
path: ./build/build/${{ env.PLUGIN_NAME }}.json
|
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
if: startsWith(matrix.config.os, 'ubuntu')
|
if: startsWith(matrix.config.os, 'ubuntu')
|
||||||
run: |
|
run: |
|
||||||
xvfb-run ./build/build/test/QodeAssistTest
|
xvfb-run ./build/build/test/QodeAssistTest
|
||||||
|
|
||||||
update_json:
|
|
||||||
if: contains(github.ref, 'tags/v')
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
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: |
|
|
||||||
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-latest
|
runs-on: ubuntu-22.04
|
||||||
needs: [build, update_json]
|
needs: [build]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||||
with:
|
with:
|
||||||
path: release-with-dirs
|
path: release-with-dirs
|
||||||
|
|
||||||
@@ -325,9 +289,21 @@ 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@v2
|
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
24
.github/workflows/check_formatting.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: Check formatting
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
format:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y clang-format-19
|
|
||||||
- name: Check formatting
|
|
||||||
run: |
|
|
||||||
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
|
|
||||||
clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
|
|
||||||
git diff --exit-code || exit 1
|
|
||||||
6
.gitignore
vendored
@@ -74,3 +74,9 @@ CMakeLists.txt.user*
|
|||||||
*.exe
|
*.exe
|
||||||
|
|
||||||
/build
|
/build
|
||||||
|
/.qodeassist
|
||||||
|
/.cursor
|
||||||
|
/.vscode
|
||||||
|
.qtc_clangd/compile_commands.json
|
||||||
|
CLAUDE.md
|
||||||
|
/.claude
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "sources/external/llmqore"]
|
||||||
|
path = sources/external/llmqore
|
||||||
|
url = https://github.com/Palm1r/llmqore.git
|
||||||
102
CMakeLists.txt
@@ -11,18 +11,34 @@ set(CMAKE_CXX_EXTENSIONS OFF)
|
|||||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
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 REQUIRED)
|
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg Test LinguistTools REQUIRED)
|
||||||
find_package(GTest)
|
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(
|
add_definitions(
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
|
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
|
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
|
||||||
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
|
||||||
)
|
)
|
||||||
|
|
||||||
add_subdirectory(llmcore)
|
add_subdirectory(sources/external/llmqore)
|
||||||
|
add_subdirectory(pluginllmcore)
|
||||||
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)
|
if(GTest_FOUND)
|
||||||
@@ -35,14 +51,19 @@ 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
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Widgets
|
Qt::Widgets
|
||||||
Qt::Network
|
Qt::Network
|
||||||
|
Qt::Svg
|
||||||
QtCreator::ExtensionSystem
|
QtCreator::ExtensionSystem
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
|
QtCreator::CPlusPlus
|
||||||
|
LLMQore
|
||||||
|
PluginLLMCore
|
||||||
QodeAssistChatViewplugin
|
QodeAssistChatViewplugin
|
||||||
SOURCES
|
SOURCES
|
||||||
.github/workflows/build_cmake.yml
|
.github/workflows/build_cmake.yml
|
||||||
@@ -52,6 +73,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
QodeAssistConstants.hpp
|
QodeAssistConstants.hpp
|
||||||
QodeAssisttr.h
|
QodeAssisttr.h
|
||||||
LLMClientInterface.hpp LLMClientInterface.cpp
|
LLMClientInterface.hpp LLMClientInterface.cpp
|
||||||
|
RefactorContextHelper.hpp
|
||||||
templates/Templates.hpp
|
templates/Templates.hpp
|
||||||
templates/CodeLlamaFim.hpp
|
templates/CodeLlamaFim.hpp
|
||||||
templates/Ollama.hpp
|
templates/Ollama.hpp
|
||||||
@@ -61,7 +83,7 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/StarCoder2Fim.hpp
|
templates/StarCoder2Fim.hpp
|
||||||
# templates/DeepSeekCoderFim.hpp
|
# templates/DeepSeekCoderFim.hpp
|
||||||
# templates/CustomFimTemplate.hpp
|
# templates/CustomFimTemplate.hpp
|
||||||
templates/Qwen.hpp
|
templates/Qwen25CoderFIM.hpp
|
||||||
templates/OpenAICompatible.hpp
|
templates/OpenAICompatible.hpp
|
||||||
templates/Llama3.hpp
|
templates/Llama3.hpp
|
||||||
templates/ChatML.hpp
|
templates/ChatML.hpp
|
||||||
@@ -70,23 +92,97 @@ add_qtc_plugin(QodeAssist
|
|||||||
templates/CodeLlamaQMLFim.hpp
|
templates/CodeLlamaQMLFim.hpp
|
||||||
templates/GoogleAI.hpp
|
templates/GoogleAI.hpp
|
||||||
templates/LlamaCppFim.hpp
|
templates/LlamaCppFim.hpp
|
||||||
|
templates/Qwen3CoderFIM.hpp
|
||||||
|
templates/OpenAIResponses.hpp
|
||||||
providers/Providers.hpp
|
providers/Providers.hpp
|
||||||
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
|
||||||
|
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.cpp
|
||||||
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
|
||||||
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
|
||||||
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
|
||||||
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
|
||||||
|
providers/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.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/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
|
||||||
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
|
||||||
|
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
|
||||||
|
providers/OpenAIResponses/ModelRequest.hpp
|
||||||
|
providers/OpenAIResponses/ResponseObject.hpp
|
||||||
|
providers/OpenAIResponses/GetResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/DeleteResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/CancelResponseRequest.hpp
|
||||||
|
providers/OpenAIResponses/ListInputItemsRequest.hpp
|
||||||
|
providers/OpenAIResponses/InputTokensRequest.hpp
|
||||||
|
providers/OpenAIResponses/ItemTypesReference.hpp
|
||||||
|
providers/OpenAIResponsesRequestBuilder.hpp
|
||||||
|
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.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/CompletionHintWidget.hpp widgets/CompletionHintWidget.cpp
|
||||||
|
widgets/CompletionHintHandler.hpp widgets/CompletionHintHandler.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
|
||||||
|
widgets/RefactorWidget.hpp widgets/RefactorWidget.cpp
|
||||||
|
widgets/RefactorWidgetHandler.hpp widgets/RefactorWidgetHandler.cpp
|
||||||
|
widgets/ContextExtractor.hpp
|
||||||
|
widgets/DiffStatistics.hpp
|
||||||
|
|
||||||
|
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
|
||||||
|
tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
|
||||||
|
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.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/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
|
||||||
|
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
|
||||||
|
tools/FindFileTool.hpp tools/FindFileTool.cpp
|
||||||
|
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
|
||||||
|
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
|
||||||
|
tools/TodoTool.hpp tools/TodoTool.cpp
|
||||||
|
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
|
||||||
|
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
|
||||||
|
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
|
||||||
|
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,17 +6,27 @@ 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 QtQuick
|
DEPENDENCIES
|
||||||
|
QtQuick
|
||||||
QML_FILES
|
QML_FILES
|
||||||
qml/RootItem.qml
|
qml/RootItem.qml
|
||||||
qml/ChatItem.qml
|
|
||||||
qml/Badge.qml
|
qml/chatparts/CodeBlock.qml
|
||||||
qml/dialog/CodeBlock.qml
|
qml/chatparts/FileEditBlock.qml
|
||||||
qml/dialog/TextBlock.qml
|
qml/chatparts/TextBlock.qml
|
||||||
qml/controls/QoAButton.qml
|
qml/chatparts/ThinkingBlock.qml
|
||||||
qml/parts/TopBar.qml
|
qml/chatparts/ToolBlock.qml
|
||||||
qml/parts/BottomBar.qml
|
qml/chatparts/ChatItem.qml
|
||||||
qml/parts/AttachedFilesPlace.qml
|
|
||||||
|
qml/controls/AttachedFilesPlace.qml
|
||||||
|
qml/controls/BottomBar.qml
|
||||||
|
qml/controls/FileMentionPopup.qml
|
||||||
|
qml/controls/FileEditsActionBar.qml
|
||||||
|
qml/controls/ContextViewer.qml
|
||||||
|
qml/controls/Toast.qml
|
||||||
|
qml/controls/TopBar.qml
|
||||||
|
qml/controls/SplitDropZone.qml
|
||||||
|
|
||||||
RESOURCES
|
RESOURCES
|
||||||
icons/attach-file-light.svg
|
icons/attach-file-light.svg
|
||||||
icons/attach-file-dark.svg
|
icons/attach-file-dark.svg
|
||||||
@@ -24,6 +34,28 @@ 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/image-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/context-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
|
||||||
|
icons/tools-icon-on.svg
|
||||||
|
icons/tools-icon-off.svg
|
||||||
|
icons/settings-icon.svg
|
||||||
|
icons/compress-icon.svg
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
ChatWidget.hpp ChatWidget.cpp
|
ChatWidget.hpp ChatWidget.cpp
|
||||||
ChatModel.hpp ChatModel.cpp
|
ChatModel.hpp ChatModel.cpp
|
||||||
@@ -32,6 +64,12 @@ 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
|
||||||
|
FileItem.hpp FileItem.cpp
|
||||||
|
ChatFileManager.hpp ChatFileManager.cpp
|
||||||
|
ChatCompressor.hpp ChatCompressor.cpp
|
||||||
|
FileMentionItem.hpp FileMentionItem.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(QodeAssistChatView
|
target_link_libraries(QodeAssistChatView
|
||||||
@@ -42,9 +80,12 @@ target_link_libraries(QodeAssistChatView
|
|||||||
Qt::Network
|
Qt::Network
|
||||||
QtCreator::Core
|
QtCreator::Core
|
||||||
QtCreator::Utils
|
QtCreator::Utils
|
||||||
LLMCore
|
PluginLLMCore
|
||||||
QodeAssistSettings
|
QodeAssistSettings
|
||||||
Context
|
Context
|
||||||
|
QodeAssistUIControlsplugin
|
||||||
|
QodeAssistLogger
|
||||||
|
LLMQore
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(QodeAssistChatView
|
target_include_directories(QodeAssistChatView
|
||||||
|
|||||||
290
ChatView/ChatCompressor.cpp
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatCompressor.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include "ChatModel.hpp"
|
||||||
|
#include "GeneralSettings.hpp"
|
||||||
|
#include "PromptTemplateManager.hpp"
|
||||||
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "logger/Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatCompressor::ChatCompressor(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
|
||||||
|
{
|
||||||
|
if (m_isCompressing) {
|
||||||
|
emit compressionFailed(tr("Compression already in progress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatFilePath.isEmpty()) {
|
||||||
|
emit compressionFailed(tr("No chat file to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatModel || chatModel->rowCount() == 0) {
|
||||||
|
emit compressionFailed(tr("Chat is empty, nothing to compress"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (!m_provider) {
|
||||||
|
emit compressionFailed(tr("No provider available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
|
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
||||||
|
templateName);
|
||||||
|
|
||||||
|
if (!promptTemplate) {
|
||||||
|
emit compressionFailed(tr("No template available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_isCompressing = true;
|
||||||
|
m_chatModel = chatModel;
|
||||||
|
m_originalChatPath = chatFilePath;
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
|
||||||
|
emit compressionStarted();
|
||||||
|
|
||||||
|
connectProviderSignals();
|
||||||
|
|
||||||
|
QJsonObject payload{
|
||||||
|
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||||
|
|
||||||
|
buildRequestPayload(payload, promptTemplate);
|
||||||
|
|
||||||
|
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
m_currentRequestId = m_provider->sendRequest(
|
||||||
|
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
|
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::isCompressing() const
|
||||||
|
{
|
||||||
|
return m_isCompressing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cancelCompression()
|
||||||
|
{
|
||||||
|
if (!m_isCompressing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE("Cancelling compression request");
|
||||||
|
|
||||||
|
if (m_provider && !m_currentRequestId.isEmpty())
|
||||||
|
m_provider->cancelRequest(m_currentRequestId);
|
||||||
|
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(tr("Compression cancelled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_accumulatedSummary += partialText;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
|
||||||
|
{
|
||||||
|
Q_UNUSED(fullText)
|
||||||
|
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
|
||||||
|
|
||||||
|
QString compressedPath = createCompressedChatPath(m_originalChatPath);
|
||||||
|
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
|
||||||
|
handleCompressionError(tr("Failed to save compressed chat"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
|
||||||
|
cleanupState();
|
||||||
|
emit compressionCompleted(compressedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
|
||||||
|
{
|
||||||
|
if (!m_isCompressing || requestId != m_currentRequestId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
|
||||||
|
handleCompressionError(tr("Compression failed: %1").arg(error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::handleCompressionError(const QString &error)
|
||||||
|
{
|
||||||
|
cleanupState();
|
||||||
|
emit compressionFailed(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(originalPath);
|
||||||
|
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
|
||||||
|
return QString("%1/%2_%3.%4")
|
||||||
|
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatCompressor::buildCompressionPrompt() const
|
||||||
|
{
|
||||||
|
return QStringLiteral(
|
||||||
|
"Please create a comprehensive summary of our entire conversation above. "
|
||||||
|
"The summary should:\n"
|
||||||
|
"1. Preserve all important context, decisions, and key information\n"
|
||||||
|
"2. Maintain technical details, code snippets, file references, and specific examples\n"
|
||||||
|
"3. Keep the chronological flow of the discussion\n"
|
||||||
|
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
|
||||||
|
"5. Be written in clear, structured format\n"
|
||||||
|
"6. Use markdown formatting for better readability\n\n"
|
||||||
|
"Create the summary now:");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::buildRequestPayload(
|
||||||
|
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
|
||||||
|
{
|
||||||
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
|
context.systemPrompt = QStringLiteral(
|
||||||
|
"You are a helpful assistant that creates concise summaries of conversations. "
|
||||||
|
"Your summaries preserve key information, technical details, and the flow of discussion.");
|
||||||
|
|
||||||
|
QVector<PluginLLMCore::Message> messages;
|
||||||
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
|
if (msg.role == ChatModel::ChatRole::Tool
|
||||||
|
|| msg.role == ChatModel::ChatRole::FileEdit
|
||||||
|
|| msg.role == ChatModel::ChatRole::Thinking)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
PluginLLMCore::Message apiMessage;
|
||||||
|
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
|
||||||
|
apiMessage.content = msg.content;
|
||||||
|
messages.append(apiMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLLMCore::Message compressionRequest;
|
||||||
|
compressionRequest.role = "user";
|
||||||
|
compressionRequest.content = buildCompressionPrompt();
|
||||||
|
messages.append(compressionRequest);
|
||||||
|
|
||||||
|
context.history = messages;
|
||||||
|
|
||||||
|
m_provider->prepareRequest(
|
||||||
|
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatCompressor::createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary)
|
||||||
|
{
|
||||||
|
QFile sourceFile(sourcePath);
|
||||||
|
if (!sourceFile.open(QIODevice::ReadOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonParseError parseError;
|
||||||
|
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
|
||||||
|
sourceFile.close();
|
||||||
|
|
||||||
|
if (doc.isNull() || !doc.isObject()) {
|
||||||
|
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
|
||||||
|
.arg(sourcePath, parseError.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject root = doc.object();
|
||||||
|
|
||||||
|
QJsonObject summaryMessage;
|
||||||
|
summaryMessage["role"] = "assistant";
|
||||||
|
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
|
||||||
|
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||||
|
summaryMessage["isRedacted"] = false;
|
||||||
|
summaryMessage["attachments"] = QJsonArray();
|
||||||
|
summaryMessage["images"] = QJsonArray();
|
||||||
|
|
||||||
|
root["messages"] = QJsonArray{summaryMessage};
|
||||||
|
|
||||||
|
if (QFile::exists(destPath))
|
||||||
|
QFile::remove(destPath);
|
||||||
|
|
||||||
|
QFile destFile(destPath);
|
||||||
|
if (!destFile.open(QIODevice::WriteOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::connectProviderSignals()
|
||||||
|
{
|
||||||
|
auto *c = m_provider->client();
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onPartialResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onFullResponseReceived,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
|
||||||
|
m_connections.append(connect(
|
||||||
|
c,
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&ChatCompressor::onRequestFailed,
|
||||||
|
Qt::UniqueConnection));
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::disconnectAllSignals()
|
||||||
|
{
|
||||||
|
for (const auto &connection : std::as_const(m_connections))
|
||||||
|
disconnect(connection);
|
||||||
|
m_connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatCompressor::cleanupState()
|
||||||
|
{
|
||||||
|
disconnectAllSignals();
|
||||||
|
|
||||||
|
m_isCompressing = false;
|
||||||
|
m_currentRequestId.clear();
|
||||||
|
m_originalChatPath.clear();
|
||||||
|
m_accumulatedSummary.clear();
|
||||||
|
m_chatModel = nullptr;
|
||||||
|
m_provider = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
63
ChatView/ChatCompressor.hpp
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace QodeAssist::PluginLLMCore {
|
||||||
|
class Provider;
|
||||||
|
class PromptTemplate;
|
||||||
|
} // namespace QodeAssist::PluginLLMCore
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatModel;
|
||||||
|
|
||||||
|
class ChatCompressor : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatCompressor(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
void cancelCompression();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void compressionStarted();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
|
||||||
|
void onFullResponseReceived(const QString &requestId, const QString &fullText);
|
||||||
|
void onRequestFailed(const QString &requestId, const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString createCompressedChatPath(const QString &originalPath) const;
|
||||||
|
QString buildCompressionPrompt() const;
|
||||||
|
bool createCompressedChatFile(
|
||||||
|
const QString &sourcePath, const QString &destPath, const QString &summary);
|
||||||
|
void connectProviderSignals();
|
||||||
|
void disconnectAllSignals();
|
||||||
|
void cleanupState();
|
||||||
|
void handleCompressionError(const QString &error);
|
||||||
|
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
|
||||||
|
|
||||||
|
bool m_isCompressing = false;
|
||||||
|
QString m_currentRequestId;
|
||||||
|
QString m_originalChatPath;
|
||||||
|
QString m_accumulatedSummary;
|
||||||
|
PluginLLMCore::Provider *m_provider = nullptr;
|
||||||
|
ChatModel *m_chatModel = nullptr;
|
||||||
|
|
||||||
|
QList<QMetaObject::Connection> m_connections;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
16
ChatView/ChatData.hpp
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
Q_NAMESPACE
|
||||||
|
QML_NAMED_ELEMENT(MessagePartType)
|
||||||
|
|
||||||
|
enum class MessagePartType { Code, Text, Image };
|
||||||
|
Q_ENUM_NS(MessagePartType)
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
190
ChatView/ChatFileManager.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
#include <QUuid>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
ChatFileManager::ChatFileManager(QObject *parent)
|
||||||
|
: QObject(parent)
|
||||||
|
, m_intermediateStorageDir(getIntermediateStorageDir())
|
||||||
|
{}
|
||||||
|
|
||||||
|
ChatFileManager::~ChatFileManager() = default;
|
||||||
|
|
||||||
|
QStringList ChatFileManager::processDroppedFiles(const QStringList &filePaths)
|
||||||
|
{
|
||||||
|
QStringList processedPaths;
|
||||||
|
processedPaths.reserve(filePaths.size());
|
||||||
|
|
||||||
|
for (const QString &filePath : filePaths) {
|
||||||
|
if (!isFileAccessible(filePath)) {
|
||||||
|
const QString error = tr("File is not accessible: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString copiedPath = copyToIntermediateStorage(filePath);
|
||||||
|
if (!copiedPath.isEmpty()) {
|
||||||
|
processedPaths.append(copiedPath);
|
||||||
|
emit fileCopiedToStorage(filePath, copiedPath);
|
||||||
|
LOG_MESSAGE(QString("File copied to storage: %1 -> %2").arg(filePath, copiedPath));
|
||||||
|
} else {
|
||||||
|
const QString error = tr("Failed to copy file: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
emit fileOperationFailed(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedPaths;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::setChatFilePath(const QString &chatFilePath)
|
||||||
|
{
|
||||||
|
m_chatFilePath = chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::clearIntermediateStorage()
|
||||||
|
{
|
||||||
|
QDir dir(m_intermediateStorageDir);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
LOG_MESSAGE(QString("Removed intermediate file: %1").arg(fileInfo.fileName()));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Failed to remove intermediate file: %1")
|
||||||
|
.arg(fileInfo.fileName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatFileManager::isFileAccessible(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
return fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatFileManager::cleanupGlobalIntermediateStorage()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir(intermediatePath);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
|
||||||
|
int removedCount = 0;
|
||||||
|
int failedCount = 0;
|
||||||
|
|
||||||
|
for (const QFileInfo &fileInfo : files) {
|
||||||
|
QFile file(fileInfo.absoluteFilePath());
|
||||||
|
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
|
||||||
|
if (file.remove()) {
|
||||||
|
removedCount++;
|
||||||
|
} else {
|
||||||
|
failedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removedCount > 0 || failedCount > 0) {
|
||||||
|
LOG_MESSAGE(QString("ChatFileManager global cleanup: removed=%1, failed=%2")
|
||||||
|
.arg(removedCount)
|
||||||
|
.arg(failedCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::copyToIntermediateStorage(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
if (!fileInfo.exists() || !fileInfo.isFile()) {
|
||||||
|
LOG_MESSAGE(QString("Source file does not exist or is not a file: %1").arg(filePath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileInfo.size() == 0) {
|
||||||
|
LOG_MESSAGE(QString("Source file is empty: %1").arg(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newFileName = generateIntermediateFileName(filePath);
|
||||||
|
const QString destinationPath = QDir(m_intermediateStorageDir).filePath(newFileName);
|
||||||
|
|
||||||
|
if (QFileInfo::exists(destinationPath)) {
|
||||||
|
QFile::remove(destinationPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!QFile::copy(filePath, destinationPath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to copy file: %1 -> %2").arg(filePath, destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile copiedFile(destinationPath);
|
||||||
|
if (!copiedFile.exists()) {
|
||||||
|
LOG_MESSAGE(QString("Copied file does not exist after copy: %1").arg(destinationPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
copiedFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
|
||||||
|
|
||||||
|
return destinationPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::getIntermediateStorageDir()
|
||||||
|
{
|
||||||
|
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
|
||||||
|
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
|
||||||
|
|
||||||
|
QDir dir;
|
||||||
|
if (!dir.exists(intermediatePath) && !dir.mkpath(intermediatePath)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create intermediate storage directory: %1")
|
||||||
|
.arg(intermediatePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return intermediatePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatFileManager::generateIntermediateFileName(const QString &originalPath)
|
||||||
|
{
|
||||||
|
const QFileInfo fileInfo(originalPath);
|
||||||
|
const QString extension = fileInfo.suffix();
|
||||||
|
QString baseName = fileInfo.completeBaseName().left(30);
|
||||||
|
|
||||||
|
static const QRegularExpression specialChars("[^a-zA-Z0-9_-]");
|
||||||
|
baseName.replace(specialChars, "_");
|
||||||
|
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
|
||||||
|
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
|
||||||
|
|
||||||
|
return QString("%1_%2_%3.%4").arg(baseName, timestamp, uuid, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
43
ChatView/ChatFileManager.hpp
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
#include <QMap>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatFileManager : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ChatFileManager(QObject *parent = nullptr);
|
||||||
|
~ChatFileManager();
|
||||||
|
|
||||||
|
QStringList processDroppedFiles(const QStringList &filePaths);
|
||||||
|
void setChatFilePath(const QString &chatFilePath);
|
||||||
|
QString chatFilePath() const;
|
||||||
|
void clearIntermediateStorage();
|
||||||
|
|
||||||
|
static bool isFileAccessible(const QString &filePath);
|
||||||
|
static void cleanupGlobalIntermediateStorage();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void fileOperationFailed(const QString &error);
|
||||||
|
void fileCopiedToStorage(const QString &originalPath, const QString &newPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString copyToIntermediateStorage(const QString &filePath);
|
||||||
|
QString getIntermediateStorageDir();
|
||||||
|
QString generateIntermediateFileName(const QString &originalPath);
|
||||||
|
|
||||||
|
QString m_chatFilePath;
|
||||||
|
QString m_intermediateStorageDir;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
|
|
||||||
@@ -1,28 +1,19 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include <utils/aspects.h>
|
#include <utils/aspects.h>
|
||||||
#include <QtCore/qjsonobject.h>
|
#include <QDateTime>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QUrl>
|
||||||
#include <QtQml>
|
#include <QtQml>
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
|
#include "Logger.hpp"
|
||||||
|
#include "context/ChangesManager.h"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -36,6 +27,21 @@ ChatModel::ChatModel(QObject *parent)
|
|||||||
&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
|
||||||
@@ -56,11 +62,54 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
|
|||||||
return message.content;
|
return message.content;
|
||||||
}
|
}
|
||||||
case Roles::Attachments: {
|
case Roles::Attachments: {
|
||||||
QStringList filenames;
|
QVariantList attachmentsList;
|
||||||
for (const auto &attachment : message.attachments) {
|
for (const auto &attachment : message.attachments) {
|
||||||
filenames << attachment.filename;
|
QVariantMap attachmentMap;
|
||||||
|
attachmentMap["fileName"] = attachment.filename;
|
||||||
|
attachmentMap["storedPath"] = attachment.content;
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
|
QString baseName = fileInfo.completeBaseName();
|
||||||
|
QString dirPath = fileInfo.absolutePath();
|
||||||
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(attachment.content);
|
||||||
|
attachmentMap["filePath"] = fullPath;
|
||||||
|
} else {
|
||||||
|
attachmentMap["filePath"] = QString();
|
||||||
}
|
}
|
||||||
return filenames;
|
|
||||||
|
attachmentsList.append(attachmentMap);
|
||||||
|
}
|
||||||
|
return attachmentsList;
|
||||||
|
}
|
||||||
|
case Roles::IsRedacted: {
|
||||||
|
return message.isRedacted;
|
||||||
|
}
|
||||||
|
case Roles::Images: {
|
||||||
|
QVariantList imagesList;
|
||||||
|
for (const auto &image : message.images) {
|
||||||
|
QVariantMap imageMap;
|
||||||
|
imageMap["fileName"] = image.fileName;
|
||||||
|
imageMap["storedPath"] = image.storedPath;
|
||||||
|
imageMap["mediaType"] = image.mediaType;
|
||||||
|
|
||||||
|
if (!m_chatFilePath.isEmpty()) {
|
||||||
|
QFileInfo fileInfo(m_chatFilePath);
|
||||||
|
QString baseName = fileInfo.completeBaseName();
|
||||||
|
QString dirPath = fileInfo.absolutePath();
|
||||||
|
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
|
||||||
|
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
|
||||||
|
imageMap["filePath"] = fullPath;
|
||||||
|
} else {
|
||||||
|
imageMap["imageUrl"] = QString();
|
||||||
|
imageMap["filePath"] = QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
imagesList.append(imageMap);
|
||||||
|
}
|
||||||
|
return imagesList;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return QVariant();
|
return QVariant();
|
||||||
@@ -73,6 +122,8 @@ 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";
|
||||||
|
roles[Roles::Images] = "images";
|
||||||
return roles;
|
return roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,28 +131,68 @@ void ChatModel::addMessage(
|
|||||||
const QString &content,
|
const QString &content,
|
||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments)
|
const QList<Context::ContentFile> &attachments,
|
||||||
|
const QList<ImageAttachment> &images,
|
||||||
|
bool isRedacted,
|
||||||
|
const QString &signature)
|
||||||
{
|
{
|
||||||
QString fullContent = content;
|
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
|
||||||
if (!attachments.isEmpty()) {
|
&& m_messages.last().role == role) {
|
||||||
fullContent += "\n\nAttached files list:";
|
|
||||||
for (const auto &attachment : attachments) {
|
|
||||||
fullContent += QString("\nname: %1\nfile content:\n%2")
|
|
||||||
.arg(attachment.filename, attachment.content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
|
|
||||||
Message &lastMessage = m_messages.last();
|
Message &lastMessage = m_messages.last();
|
||||||
lastMessage.content = content;
|
lastMessage.content = content;
|
||||||
lastMessage.attachments = attachments;
|
lastMessage.attachments = attachments;
|
||||||
|
lastMessage.images = images;
|
||||||
|
lastMessage.isRedacted = isRedacted;
|
||||||
|
lastMessage.signature = signature;
|
||||||
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
|
||||||
} else {
|
} else {
|
||||||
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
|
||||||
Message newMessage{role, content, id};
|
Message newMessage{role, content, id};
|
||||||
newMessage.attachments = attachments;
|
newMessage.attachments = attachments;
|
||||||
|
newMessage.images = images;
|
||||||
|
newMessage.isRedacted = isRedacted;
|
||||||
|
newMessage.signature = signature;
|
||||||
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,17 +222,47 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
|
|||||||
QString textBetween
|
QString textBetween
|
||||||
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
|
||||||
if (!textBetween.isEmpty()) {
|
if (!textBetween.isEmpty()) {
|
||||||
parts.append({MessagePart::Text, textBetween, ""});
|
MessagePart part;
|
||||||
|
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()) {
|
|
||||||
parts.append({MessagePart::Text, remainingText, ""});
|
QRegularExpression unclosedBlockRegex("```(\\w*)\\n?([\\s\\S]*)$");
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,6 +283,9 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -197,4 +321,265 @@ 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) + "...]";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < m_messages.size(); ++i) {
|
||||||
|
if (m_messages[i].role == ChatRole::Thinking && m_messages[i].id == requestId) {
|
||||||
|
m_messages[i].content = displayContent;
|
||||||
|
m_messages[i].signature = signature;
|
||||||
|
emit dataChanged(index(i), index(i));
|
||||||
|
LOG_MESSAGE(QString("Updated existing thinking message at index %1").arg(i));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatModel::setChatFilePath(const QString &filePath)
|
||||||
|
{
|
||||||
|
m_chatFilePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatModel::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
@@ -37,18 +21,29 @@ class ChatModel : public QAbstractListModel
|
|||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
enum ChatRole { System, User, Assistant };
|
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
|
||||||
Q_ENUM(ChatRole)
|
Q_ENUM(ChatRole)
|
||||||
|
|
||||||
enum Roles { RoleType = Qt::UserRole, Content, Attachments };
|
enum Roles { RoleType = Qt::UserRole, Content, Attachments, IsRedacted, Images };
|
||||||
|
Q_ENUM(Roles)
|
||||||
|
|
||||||
|
struct ImageAttachment
|
||||||
|
{
|
||||||
|
QString fileName; // Original filename
|
||||||
|
QString storedPath; // Path to stored image file (relative to chat folder)
|
||||||
|
QString mediaType; // MIME type
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
|
QList<ImageAttachment> images;
|
||||||
};
|
};
|
||||||
|
|
||||||
explicit ChatModel(QObject *parent = nullptr);
|
explicit ChatModel(QObject *parent = nullptr);
|
||||||
@@ -61,7 +56,10 @@ public:
|
|||||||
const QString &content,
|
const QString &content,
|
||||||
ChatRole role,
|
ChatRole role,
|
||||||
const QString &id,
|
const QString &id,
|
||||||
const QList<Context::ContentFile> &attachments = {});
|
const QList<Context::ContentFile> &attachments = {},
|
||||||
|
const QList<ImageAttachment> &images = {},
|
||||||
|
bool isRedacted = false,
|
||||||
|
const QString &signature = QString());
|
||||||
Q_INVOKABLE void clear();
|
Q_INVOKABLE void clear();
|
||||||
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
|
||||||
|
|
||||||
@@ -73,12 +71,41 @@ 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;
|
||||||
|
|
||||||
|
void setChatFilePath(const QString &filePath);
|
||||||
|
QString chatFilePath() 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;
|
||||||
|
QString m_chatFilePath;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,32 +1,21 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#include <QQuickItem>
|
#include <QQuickItem>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
#include "ChatFileManager.hpp"
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
#include "pluginllmcore/PromptProviderChat.hpp"
|
||||||
#include <coreplugin/editormanager/editormanager.h>
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class ChatCompressor;
|
||||||
|
|
||||||
class ChatRootView : public QQuickItem
|
class ChatRootView : public QQuickItem
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -37,6 +26,32 @@ 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 useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
|
||||||
|
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged 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)
|
||||||
|
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
|
||||||
|
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
|
||||||
|
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
|
||||||
|
|
||||||
QML_ELEMENT
|
QML_ELEMENT
|
||||||
|
|
||||||
@@ -54,17 +69,27 @@ public:
|
|||||||
|
|
||||||
void autosave();
|
void autosave();
|
||||||
QString getAutosaveFilePath() const;
|
QString getAutosaveFilePath() const;
|
||||||
|
QString getAutosaveFilePath(const QString &firstMessage, const QStringList &attachments) const;
|
||||||
|
|
||||||
QStringList attachmentFiles() const;
|
QStringList attachmentFiles() const;
|
||||||
QStringList linkedFiles() const;
|
QStringList linkedFiles() const;
|
||||||
|
|
||||||
Q_INVOKABLE void showAttachFilesDialog();
|
Q_INVOKABLE void showAttachFilesDialog();
|
||||||
|
Q_INVOKABLE void addFilesToAttachList(const QStringList &filePaths);
|
||||||
Q_INVOKABLE void removeFileFromAttachList(int index);
|
Q_INVOKABLE void removeFileFromAttachList(int index);
|
||||||
Q_INVOKABLE void showLinkFilesDialog();
|
Q_INVOKABLE void showLinkFilesDialog();
|
||||||
|
Q_INVOKABLE void addFilesToLinkList(const QStringList &filePaths);
|
||||||
Q_INVOKABLE void removeFileFromLinkList(int index);
|
Q_INVOKABLE void removeFileFromLinkList(int index);
|
||||||
|
Q_INVOKABLE QStringList convertUrlsToLocalPaths(const QVariantList &urls) const;
|
||||||
|
Q_INVOKABLE void showAddImageDialog();
|
||||||
|
Q_INVOKABLE bool isImageFile(const QString &filePath) const;
|
||||||
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 openSettings();
|
||||||
|
|
||||||
|
Q_INVOKABLE void openFileInEditor(const QString &filePath);
|
||||||
|
|
||||||
Q_INVOKABLE void updateInputTokensCount();
|
Q_INVOKABLE void updateInputTokensCount();
|
||||||
int inputTokensCount() const;
|
int inputTokensCount() const;
|
||||||
@@ -76,7 +101,68 @@ public:
|
|||||||
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
|
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
|
||||||
|
|
||||||
QString chatFileName() const;
|
QString chatFileName() const;
|
||||||
|
Q_INVOKABLE QString chatFilePath() 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 useTools() const;
|
||||||
|
void setUseTools(bool enabled);
|
||||||
|
bool useThinking() const;
|
||||||
|
void setUseThinking(bool enabled);
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
Q_INVOKABLE void loadAvailableConfigurations();
|
||||||
|
Q_INVOKABLE void applyConfiguration(const QString &configName);
|
||||||
|
QStringList availableConfigurations() const;
|
||||||
|
QString currentConfiguration() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE void compressCurrentChat();
|
||||||
|
Q_INVOKABLE void cancelCompression();
|
||||||
|
|
||||||
|
Q_INVOKABLE void loadAvailableAgentRoles();
|
||||||
|
Q_INVOKABLE void applyAgentRole(const QString &roleId);
|
||||||
|
Q_INVOKABLE void openAgentRolesSettings();
|
||||||
|
QStringList availableAgentRoles() const;
|
||||||
|
QString currentAgentRole() const;
|
||||||
|
QString baseSystemPrompt() const;
|
||||||
|
QString currentAgentRoleDescription() const;
|
||||||
|
QString currentAgentRoleSystemPrompt() const;
|
||||||
|
|
||||||
|
int currentMessageTotalEdits() const;
|
||||||
|
int currentMessageAppliedEdits() const;
|
||||||
|
int currentMessagePendingEdits() const;
|
||||||
|
int currentMessageRejectedEdits() const;
|
||||||
|
|
||||||
|
QString lastInfoMessage() const;
|
||||||
|
|
||||||
|
bool isThinkingSupport() const;
|
||||||
|
|
||||||
|
bool isCompressing() const;
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void sendMessage(const QString &message);
|
void sendMessage(const QString &message);
|
||||||
@@ -84,6 +170,7 @@ public slots:
|
|||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
void clearAttachmentFiles();
|
void clearAttachmentFiles();
|
||||||
void clearLinkedFiles();
|
void clearLinkedFiles();
|
||||||
|
void clearMessages();
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void chatModelChanged();
|
void chatModelChanged();
|
||||||
@@ -93,13 +180,48 @@ 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 useToolsChanged();
|
||||||
|
void useThinkingChanged();
|
||||||
|
void currentMessageEditsStatsChanged();
|
||||||
|
|
||||||
|
void isThinkingSupportChanged();
|
||||||
|
void availableConfigurationsChanged();
|
||||||
|
void currentConfigurationChanged();
|
||||||
|
|
||||||
|
void availableAgentRolesChanged();
|
||||||
|
void currentAgentRoleChanged();
|
||||||
|
void baseSystemPromptChanged();
|
||||||
|
|
||||||
|
void isCompressingChanged();
|
||||||
|
void compressionCompleted(const QString &compressedChatPath);
|
||||||
|
void compressionFailed(const QString &error);
|
||||||
|
|
||||||
|
void openFilesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void updateFileEditStatus(const QString &editId, const QString &status);
|
||||||
QString getChatsHistoryDir() const;
|
QString getChatsHistoryDir() const;
|
||||||
QString getSuggestedFileName() const;
|
QString getSuggestedFileName() const;
|
||||||
|
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
|
||||||
|
bool hasImageAttachments(const QStringList &attachments) const;
|
||||||
|
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
|
PluginLLMCore::PromptProviderChat m_promptProvider;
|
||||||
ClientInterface *m_clientInterface;
|
ClientInterface *m_clientInterface;
|
||||||
|
ChatFileManager *m_fileManager;
|
||||||
QString m_currentTemplate;
|
QString m_currentTemplate;
|
||||||
QString m_recentFilePath;
|
QString m_recentFilePath;
|
||||||
QStringList m_attachmentFiles;
|
QStringList m_attachmentFiles;
|
||||||
@@ -108,6 +230,24 @@ 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;
|
||||||
|
|
||||||
|
QString m_currentMessageRequestId;
|
||||||
|
int m_currentMessageTotalEdits{0};
|
||||||
|
int m_currentMessageAppliedEdits{0};
|
||||||
|
int m_currentMessagePendingEdits{0};
|
||||||
|
int m_currentMessageRejectedEdits{0};
|
||||||
|
QString m_lastInfoMessage;
|
||||||
|
|
||||||
|
QStringList m_availableConfigurations;
|
||||||
|
QString m_currentConfiguration;
|
||||||
|
|
||||||
|
QStringList m_availableAgentRoles;
|
||||||
|
QString m_currentAgentRole;
|
||||||
|
|
||||||
|
ChatCompressor *m_chatCompressor;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ChatSerializer.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
const QString ChatSerializer::VERSION = "0.1";
|
const QString ChatSerializer::VERSION = "0.2";
|
||||||
|
|
||||||
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
|
||||||
{
|
{
|
||||||
@@ -40,7 +27,7 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
|
|||||||
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root = serializeChat(model);
|
QJsonObject root = serializeChat(model, filePath);
|
||||||
QJsonDocument doc(root);
|
QJsonDocument doc(root);
|
||||||
|
|
||||||
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
|
||||||
@@ -70,36 +57,96 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
|
|||||||
return {false, QString("Unsupported version: %1").arg(version)};
|
return {false, QString("Unsupported version: %1").arg(version)};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!deserializeChat(model, root)) {
|
if (!deserializeChat(model, root, filePath)) {
|
||||||
return {false, "Failed to deserialize chat data"};
|
return {false, "Failed to deserialize chat data"};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {true, QString()};
|
return {true, QString()};
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
|
QJsonObject ChatSerializer::serializeMessage(
|
||||||
|
const ChatModel::Message &message, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonObject messageObj;
|
QJsonObject messageObj;
|
||||||
messageObj["role"] = static_cast<int>(message.role);
|
messageObj["role"] = static_cast<int>(message.role);
|
||||||
messageObj["content"] = message.content;
|
messageObj["content"] = message.content;
|
||||||
messageObj["id"] = message.id;
|
messageObj["id"] = message.id;
|
||||||
|
|
||||||
|
if (message.isRedacted) {
|
||||||
|
messageObj["isRedacted"] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.signature.isEmpty()) {
|
||||||
|
messageObj["signature"] = message.signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.attachments.isEmpty()) {
|
||||||
|
QJsonArray attachmentsArray;
|
||||||
|
for (const auto &attachment : message.attachments) {
|
||||||
|
QJsonObject attachmentObj;
|
||||||
|
attachmentObj["fileName"] = attachment.filename;
|
||||||
|
attachmentObj["storedPath"] = attachment.content;
|
||||||
|
attachmentsArray.append(attachmentObj);
|
||||||
|
}
|
||||||
|
messageObj["attachments"] = attachmentsArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.images.isEmpty()) {
|
||||||
|
QJsonArray imagesArray;
|
||||||
|
for (const auto &image : message.images) {
|
||||||
|
QJsonObject imageObj;
|
||||||
|
imageObj["fileName"] = image.fileName;
|
||||||
|
imageObj["storedPath"] = image.storedPath;
|
||||||
|
imageObj["mediaType"] = image.mediaType;
|
||||||
|
imagesArray.append(imageObj);
|
||||||
|
}
|
||||||
|
messageObj["images"] = imagesArray;
|
||||||
|
}
|
||||||
|
|
||||||
return messageObj;
|
return messageObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
|
ChatModel::Message ChatSerializer::deserializeMessage(
|
||||||
|
const QJsonObject &json, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
ChatModel::Message message;
|
ChatModel::Message message;
|
||||||
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
|
||||||
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();
|
||||||
|
|
||||||
|
if (json.contains("attachments")) {
|
||||||
|
QJsonArray attachmentsArray = json["attachments"].toArray();
|
||||||
|
for (const auto &attachmentValue : attachmentsArray) {
|
||||||
|
QJsonObject attachmentObj = attachmentValue.toObject();
|
||||||
|
Context::ContentFile attachment;
|
||||||
|
attachment.filename = attachmentObj["fileName"].toString();
|
||||||
|
attachment.content = attachmentObj["storedPath"].toString();
|
||||||
|
message.attachments.append(attachment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json.contains("images")) {
|
||||||
|
QJsonArray imagesArray = json["images"].toArray();
|
||||||
|
for (const auto &imageValue : imagesArray) {
|
||||||
|
QJsonObject imageObj = imageValue.toObject();
|
||||||
|
ChatModel::ImageAttachment image;
|
||||||
|
image.fileName = imageObj["fileName"].toString();
|
||||||
|
image.storedPath = imageObj["storedPath"].toString();
|
||||||
|
image.mediaType = imageObj["mediaType"].toString();
|
||||||
|
message.images.append(image);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
|
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray;
|
QJsonArray messagesArray;
|
||||||
for (const auto &message : model->getChatHistory()) {
|
for (const auto &message : model->getChatHistory()) {
|
||||||
messagesArray.append(serializeMessage(message));
|
messagesArray.append(serializeMessage(message, chatFilePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject root;
|
QJsonObject root;
|
||||||
@@ -109,21 +156,38 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ChatSerializer::deserializeChat(ChatModel *model, const QJsonObject &json)
|
bool ChatSerializer::deserializeChat(
|
||||||
|
ChatModel *model, const QJsonObject &json, const QString &chatFilePath)
|
||||||
{
|
{
|
||||||
QJsonArray messagesArray = json["messages"].toArray();
|
QJsonArray messagesArray = json["messages"].toArray();
|
||||||
QVector<ChatModel::Message> messages;
|
QVector<ChatModel::Message> messages;
|
||||||
messages.reserve(messagesArray.size());
|
messages.reserve(messagesArray.size());
|
||||||
|
|
||||||
for (const auto &messageValue : messagesArray) {
|
for (const auto &messageValue : messagesArray) {
|
||||||
messages.append(deserializeMessage(messageValue.toObject()));
|
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
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,
|
||||||
|
message.attachments,
|
||||||
|
message.images,
|
||||||
|
message.isRedacted,
|
||||||
|
message.signature);
|
||||||
|
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
|
||||||
|
.arg(message.images.size())
|
||||||
|
.arg(message.isRedacted)
|
||||||
|
.arg(message.signature.length()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model->setLoadingFromHistory(false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +200,88 @@ bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
|
|||||||
|
|
||||||
bool ChatSerializer::validateVersion(const QString &version)
|
bool ChatSerializer::validateVersion(const QString &version)
|
||||||
{
|
{
|
||||||
return version == VERSION;
|
if (version == VERSION) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version == "0.1") {
|
||||||
|
LOG_MESSAGE(
|
||||||
|
"Loading chat from old format 0.1 - images folder structure has changed from _images "
|
||||||
|
"to _content");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
|
||||||
|
{
|
||||||
|
QFileInfo fileInfo(chatFilePath);
|
||||||
|
QString baseName = fileInfo.completeBaseName();
|
||||||
|
QString dirPath = fileInfo.absolutePath();
|
||||||
|
return QDir(dirPath).filePath(baseName + "_content");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ChatSerializer::saveContentToStorage(
|
||||||
|
const QString &chatFilePath,
|
||||||
|
const QString &fileName,
|
||||||
|
const QString &base64Data,
|
||||||
|
QString &storedPath)
|
||||||
|
{
|
||||||
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
|
QDir dir;
|
||||||
|
if (!dir.exists(contentFolder)) {
|
||||||
|
if (!dir.mkpath(contentFolder)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to create content folder: %1").arg(contentFolder));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QFileInfo originalFileInfo(fileName);
|
||||||
|
QString extension = originalFileInfo.suffix();
|
||||||
|
QString baseName = originalFileInfo.completeBaseName();
|
||||||
|
QString uniqueName = QString("%1_%2.%3")
|
||||||
|
.arg(baseName)
|
||||||
|
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
|
||||||
|
.arg(extension);
|
||||||
|
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(uniqueName);
|
||||||
|
|
||||||
|
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
|
||||||
|
QFile file(fullPath);
|
||||||
|
if (!file.open(QIODevice::WriteOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.write(contentData) == -1) {
|
||||||
|
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
storedPath = uniqueName;
|
||||||
|
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
|
||||||
|
{
|
||||||
|
QString contentFolder = getChatContentFolder(chatFilePath);
|
||||||
|
QString fullPath = QDir(contentFolder).filePath(storedPath);
|
||||||
|
|
||||||
|
QFile file(fullPath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray contentData = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return contentData.toBase64();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
@@ -40,10 +24,18 @@ public:
|
|||||||
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
|
||||||
|
|
||||||
// Public for testing purposes
|
// Public for testing purposes
|
||||||
static QJsonObject serializeMessage(const ChatModel::Message &message);
|
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
|
||||||
static ChatModel::Message deserializeMessage(const QJsonObject &json);
|
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
|
||||||
static QJsonObject serializeChat(const ChatModel *model);
|
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
|
||||||
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
|
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
|
||||||
|
|
||||||
|
// Content management (images and text files)
|
||||||
|
static QString getChatContentFolder(const QString &chatFilePath);
|
||||||
|
static bool saveContentToStorage(const QString &chatFilePath,
|
||||||
|
const QString &fileName,
|
||||||
|
const QString &base64Data,
|
||||||
|
QString &storedPath);
|
||||||
|
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static const QString VERSION;
|
static const QString VERSION;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ChatUtils.h"
|
#include "ChatUtils.h"
|
||||||
|
|
||||||
@@ -29,4 +13,40 @@ 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
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
@@ -34,6 +18,7 @@ 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
|
||||||
|
|||||||
90
ChatView/ChatView.cpp
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
35
ChatView/ChatView.hpp
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ChatWidget.hpp"
|
#include "ChatWidget.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,84 +1,133 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ClientInterface.hpp"
|
#include "ClientInterface.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
|
||||||
|
#include <projectexplorer/buildconfiguration.h>
|
||||||
|
#include <projectexplorer/target.h>
|
||||||
#include <texteditor/textdocument.h>
|
#include <texteditor/textdocument.h>
|
||||||
|
#include <QFile>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QImageReader>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
|
#include <QMimeDatabase>
|
||||||
#include <QUuid>
|
#include <QUuid>
|
||||||
|
|
||||||
#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 <LLMQore/ToolsManager.hpp>
|
||||||
|
|
||||||
|
#include "tools/TodoTool.hpp"
|
||||||
|
|
||||||
#include "ChatAssistantSettings.hpp"
|
#include "ChatAssistantSettings.hpp"
|
||||||
#include "ContextManager.hpp"
|
#include "ChatSerializer.hpp"
|
||||||
#include "GeneralSettings.hpp"
|
#include "GeneralSettings.hpp"
|
||||||
#include "Logger.hpp"
|
#include "Logger.hpp"
|
||||||
#include "PromptTemplateManager.hpp"
|
|
||||||
#include "ProvidersManager.hpp"
|
#include "ProvidersManager.hpp"
|
||||||
|
#include "ToolsSettings.hpp"
|
||||||
|
#include <RulesLoader.hpp>
|
||||||
|
#include <context/ChangesManager.h>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
|
ClientInterface::ClientInterface(
|
||||||
|
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_requestHandler(new LLMCore::RequestHandler(this))
|
|
||||||
, m_chatModel(chatModel)
|
, m_chatModel(chatModel)
|
||||||
{
|
, m_promptProvider(promptProvider)
|
||||||
connect(
|
, m_contextManager(new Context::ContextManager(this))
|
||||||
m_requestHandler,
|
{}
|
||||||
&LLMCore::RequestHandler::completionReceived,
|
|
||||||
this,
|
|
||||||
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
|
|
||||||
handleLLMResponse(completion, request, isComplete);
|
|
||||||
});
|
|
||||||
|
|
||||||
connect(
|
ClientInterface::~ClientInterface()
|
||||||
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(
|
|
||||||
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
|
|
||||||
{
|
{
|
||||||
cancelRequest();
|
cancelRequest();
|
||||||
|
}
|
||||||
|
|
||||||
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
|
void ClientInterface::sendMessage(
|
||||||
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
|
const QString &message,
|
||||||
|
const QList<QString> &attachments,
|
||||||
|
const QList<QString> &linkedFiles,
|
||||||
|
bool useTools,
|
||||||
|
bool useThinking)
|
||||||
|
{
|
||||||
|
cancelRequest();
|
||||||
|
m_accumulatedResponses.clear();
|
||||||
|
|
||||||
|
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
|
||||||
|
|
||||||
|
QList<QString> imageFiles;
|
||||||
|
QList<QString> textFiles;
|
||||||
|
|
||||||
|
for (const QString &filePath : attachments) {
|
||||||
|
if (isImageFile(filePath)) {
|
||||||
|
imageFiles.append(filePath);
|
||||||
|
} else {
|
||||||
|
textFiles.append(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<Context::ContentFile> storedAttachments;
|
||||||
|
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
auto attachFiles = m_contextManager->getContentFiles(textFiles);
|
||||||
|
for (const auto &file : attachFiles) {
|
||||||
|
QString storedPath;
|
||||||
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) {
|
||||||
|
Context::ContentFile storedFile;
|
||||||
|
storedFile.filename = file.filename;
|
||||||
|
storedFile.content = storedPath;
|
||||||
|
storedAttachments.append(storedFile);
|
||||||
|
LOG_MESSAGE(QString("Stored text file %1 as %2").arg(file.filename, storedPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!textFiles.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 text file(s)")
|
||||||
|
.arg(textFiles.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<ChatModel::ImageAttachment> imageAttachments;
|
||||||
|
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
for (const QString &imagePath : imageFiles) {
|
||||||
|
QString base64Data = encodeImageToBase64(imagePath);
|
||||||
|
if (base64Data.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString storedPath;
|
||||||
|
QFileInfo fileInfo(imagePath);
|
||||||
|
if (ChatSerializer::saveContentToStorage(
|
||||||
|
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
|
||||||
|
ChatModel::ImageAttachment imageAttachment;
|
||||||
|
imageAttachment.fileName = fileInfo.fileName();
|
||||||
|
imageAttachment.storedPath = storedPath;
|
||||||
|
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
|
||||||
|
imageAttachments.append(imageAttachment);
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (!imageFiles.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)")
|
||||||
|
.arg(imageFiles.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
|
||||||
|
|
||||||
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
auto &chatAssistantSettings = Settings::chatAssistantSettings();
|
||||||
|
|
||||||
auto providerName = Settings::generalSettings().caProvider();
|
auto providerName = Settings::generalSettings().caProvider();
|
||||||
auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
||||||
@@ -86,85 +135,224 @@ void ClientInterface::sendMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto templateName = Settings::generalSettings().caTemplate();
|
auto templateName = Settings::generalSettings().caTemplate();
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
|
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||||
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));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData context;
|
PluginLLMCore::ContextData context;
|
||||||
|
|
||||||
|
const bool isToolsEnabled = useTools;
|
||||||
|
|
||||||
if (chatAssistantSettings.useSystemPrompt()) {
|
if (chatAssistantSettings.useSystemPrompt()) {
|
||||||
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
QString systemPrompt = chatAssistantSettings.systemPrompt();
|
||||||
|
|
||||||
|
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
|
||||||
|
if (!lastRoleId.isEmpty()) {
|
||||||
|
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
|
||||||
|
if (!role.id.isEmpty())
|
||||||
|
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto project = PluginLLMCore::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());
|
||||||
|
|
||||||
|
if (auto target = project->activeTarget()) {
|
||||||
|
if (auto buildConfig = target->activeBuildConfiguration()) {
|
||||||
|
systemPrompt += QString("\n# Active Build directory: %1")
|
||||||
|
.arg(buildConfig->buildDirectory().toUrlishString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString projectRules
|
||||||
|
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::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;
|
context.systemPrompt = systemPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector<LLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
for (const auto &msg : m_chatModel->getChatHistory()) {
|
for (const auto &msg : m_chatModel->getChatHistory()) {
|
||||||
messages.append({msg.role == ChatModel::ChatRole::User ? "user" : "assistant", msg.content});
|
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PluginLLMCore::Message apiMessage;
|
||||||
|
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
|
||||||
|
apiMessage.content = msg.content;
|
||||||
|
|
||||||
|
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
|
||||||
|
apiMessage.content += "\n\nAttached files:";
|
||||||
|
for (const auto &attachment : msg.attachments) {
|
||||||
|
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
|
||||||
|
if (!fileContent.isEmpty()) {
|
||||||
|
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
|
||||||
|
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
|
||||||
|
.arg(attachment.filename, decodedContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
|
||||||
|
apiMessage.isRedacted = msg.isRedacted;
|
||||||
|
apiMessage.signature = msg.signature;
|
||||||
|
|
||||||
|
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
|
||||||
|
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
|
||||||
|
auto apiImages = loadImagesFromStorage(msg.images);
|
||||||
|
if (!apiImages.isEmpty()) {
|
||||||
|
apiMessage.images = apiImages;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.append(apiMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageFiles.isEmpty()
|
||||||
|
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
|
||||||
|
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
|
||||||
|
.arg(provider->name(), QString::number(imageFiles.size())));
|
||||||
|
}
|
||||||
|
|
||||||
context.history = messages;
|
context.history = messages;
|
||||||
|
|
||||||
LLMCore::LLMConfig config;
|
QJsonObject payload{
|
||||||
config.requestType = LLMCore::RequestType::Chat;
|
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
provider->prepareRequest(
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
payload,
|
||||||
QString stream = chatAssistantSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
promptTemplate,
|
||||||
: QString{"generateContent?"};
|
context,
|
||||||
config.url = QUrl(QString("%1/models/%2:%3")
|
PluginLLMCore::RequestType::Chat,
|
||||||
.arg(
|
useTools,
|
||||||
Settings::generalSettings().caUrl(),
|
useThinking);
|
||||||
Settings::generalSettings().caModel(),
|
|
||||||
stream));
|
provider->client()->setMaxToolContinuations(
|
||||||
} else {
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
config.url
|
|
||||||
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
|
connect(
|
||||||
config.providerRequest
|
provider->client(),
|
||||||
= {{"model", Settings::generalSettings().caModel()},
|
&::LLMQore::BaseClient::chunkReceived,
|
||||||
{"stream", chatAssistantSettings.stream()}};
|
this,
|
||||||
|
&ClientInterface::handlePartialResponse,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleFullResponse,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleRequestFailed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::toolStarted,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleToolExecutionStarted,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::toolResultReady,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleToolExecutionCompleted,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::thinkingBlockReceived,
|
||||||
|
this,
|
||||||
|
&ClientInterface::handleThinkingBlockReceived,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
|
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
|
||||||
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
|
||||||
|
emit requestStarted(requestId);
|
||||||
|
|
||||||
|
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||||
|
&& provider->toolsManager()) {
|
||||||
|
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||||
|
provider->toolsManager()->tool("todo_tool"))) {
|
||||||
|
todoTool->setCurrentSessionId(m_chatFilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.apiKey = provider->apiKey();
|
|
||||||
|
|
||||||
config.provider
|
|
||||||
->prepareRequest(config.providerRequest, promptTemplate, context, LLMCore::RequestType::Chat);
|
|
||||||
|
|
||||||
QJsonObject request{{"id", QUuid::createUuid().toString()}};
|
|
||||||
m_requestHandler->sendLLMRequest(config, request);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::clearMessages()
|
void ClientInterface::clearMessages()
|
||||||
{
|
{
|
||||||
|
const auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (provider && !m_chatFilePath.isEmpty()
|
||||||
|
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||||
|
&& provider->toolsManager()) {
|
||||||
|
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||||
|
provider->toolsManager()->tool("todo_tool"))) {
|
||||||
|
todoTool->clearSession(m_chatFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
m_chatModel->clear();
|
m_chatModel->clear();
|
||||||
LOG_MESSAGE("Chat history cleared");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::cancelRequest()
|
void ClientInterface::cancelRequest()
|
||||||
{
|
{
|
||||||
auto id = m_chatModel->lastMessageId();
|
QSet<PluginLLMCore::Provider *> providers;
|
||||||
m_requestHandler->cancelRequest(id);
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
|
if (it.value().provider) {
|
||||||
|
providers.insert(it.value().provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto *provider : providers) {
|
||||||
|
disconnect(provider->client(), 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();
|
||||||
|
m_awaitingContinuation.clear();
|
||||||
|
|
||||||
|
LOG_MESSAGE("All requests cancelled and state cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClientInterface::handleLLMResponse(
|
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +388,7 @@ QString ClientInterface::getSystemPromptWithLinkedFiles(
|
|||||||
if (!linkedFiles.isEmpty()) {
|
if (!linkedFiles.isEmpty()) {
|
||||||
updatedPrompt += "\n\nLinked files for reference:\n";
|
updatedPrompt += "\n\nLinked files for reference:\n";
|
||||||
|
|
||||||
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
|
auto contentFiles = m_contextManager->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);
|
||||||
}
|
}
|
||||||
@@ -209,4 +397,213 @@ QString ClientInterface::getSystemPromptWithLinkedFiles(
|
|||||||
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;
|
||||||
|
|
||||||
|
if (m_awaitingContinuation.remove(requestId)) {
|
||||||
|
m_accumulatedResponses[requestId].clear();
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
m_accumulatedResponses.remove(requestId);
|
||||||
|
m_awaitingContinuation.remove(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
|
||||||
|
{
|
||||||
|
auto it = m_activeRequests.find(requestId);
|
||||||
|
if (it == m_activeRequests.end())
|
||||||
|
return;
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
|
||||||
|
emit errorOccurred(error);
|
||||||
|
|
||||||
|
m_activeRequests.erase(it);
|
||||||
|
m_accumulatedResponses.remove(requestId);
|
||||||
|
m_awaitingContinuation.remove(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleThinkingBlockReceived(
|
||||||
|
const QString &requestId, const QString &thinking, const QString &signature)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_awaitingContinuation.remove(requestId)) {
|
||||||
|
m_accumulatedResponses[requestId].clear();
|
||||||
|
LOG_MESSAGE(
|
||||||
|
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (thinking.isEmpty()) {
|
||||||
|
m_chatModel->addRedactedThinkingBlock(requestId, signature);
|
||||||
|
} else {
|
||||||
|
m_chatModel->addThinkingBlock(requestId, thinking, signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleToolExecutionStarted(
|
||||||
|
const QString &requestId, const QString &toolId, const QString &toolName)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
|
||||||
|
m_awaitingContinuation.insert(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::handleToolExecutionCompleted(
|
||||||
|
const QString &requestId,
|
||||||
|
const QString &toolId,
|
||||||
|
const QString &toolName,
|
||||||
|
const QString &toolOutput)
|
||||||
|
{
|
||||||
|
if (!m_activeRequests.contains(requestId)) {
|
||||||
|
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ClientInterface::isImageFile(const QString &filePath) const
|
||||||
|
{
|
||||||
|
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
QString extension = fileInfo.suffix().toLower();
|
||||||
|
|
||||||
|
return imageExtensions.contains(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
|
||||||
|
{
|
||||||
|
static const QHash<QString, QString> mediaTypes
|
||||||
|
= {{"png", "image/png"},
|
||||||
|
{"jpg", "image/jpeg"},
|
||||||
|
{"jpeg", "image/jpeg"},
|
||||||
|
{"gif", "image/gif"},
|
||||||
|
{"webp", "image/webp"},
|
||||||
|
{"bmp", "image/bmp"},
|
||||||
|
{"svg", "image/svg+xml"}};
|
||||||
|
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
QString extension = fileInfo.suffix().toLower();
|
||||||
|
|
||||||
|
if (mediaTypes.contains(extension)) {
|
||||||
|
return mediaTypes[extension];
|
||||||
|
}
|
||||||
|
|
||||||
|
QMimeDatabase mimeDb;
|
||||||
|
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
|
||||||
|
return mimeType.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::encodeImageToBase64(const QString &filePath) const
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath));
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray imageData = file.readAll();
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
return imageData.toBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
|
||||||
|
const QList<ChatModel::ImageAttachment> &storedImages) const
|
||||||
|
{
|
||||||
|
QVector<PluginLLMCore::ImageAttachment> apiImages;
|
||||||
|
|
||||||
|
for (const auto &storedImage : storedImages) {
|
||||||
|
QString base64Data
|
||||||
|
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
|
||||||
|
if (base64Data.isEmpty()) {
|
||||||
|
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLLMCore::ImageAttachment apiImage;
|
||||||
|
apiImage.data = base64Data;
|
||||||
|
apiImage.mediaType = storedImage.mediaType;
|
||||||
|
apiImage.isUrl = false;
|
||||||
|
|
||||||
|
apiImages.append(apiImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiImages;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ClientInterface::setChatFilePath(const QString &filePath)
|
||||||
|
{
|
||||||
|
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
|
||||||
|
const auto providerName = Settings::generalSettings().caProvider();
|
||||||
|
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
||||||
|
|
||||||
|
if (provider
|
||||||
|
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
|
||||||
|
&& provider->toolsManager()) {
|
||||||
|
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
|
||||||
|
provider->toolsManager()->tool("todo_tool"))) {
|
||||||
|
todoTool->clearSession(m_chatFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m_chatFilePath = filePath;
|
||||||
|
m_chatModel->setChatFilePath(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString ClientInterface::chatFilePath() const
|
||||||
|
{
|
||||||
|
return m_chatFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
@@ -1,30 +1,17 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QSet>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
#include <QVector>
|
#include <QVector>
|
||||||
|
|
||||||
#include "ChatModel.hpp"
|
#include "ChatModel.hpp"
|
||||||
#include "RequestHandler.hpp"
|
#include "Provider.hpp"
|
||||||
|
#include "pluginllmcore/IPromptProvider.hpp"
|
||||||
|
#include <context/ContextManager.hpp>
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
@@ -33,28 +20,67 @@ class ClientInterface : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit ClientInterface(ChatModel *chatModel, QObject *parent = nullptr);
|
explicit ClientInterface(
|
||||||
|
ChatModel *chatModel, PluginLLMCore::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 useTools = false,
|
||||||
|
bool useThinking = false);
|
||||||
void clearMessages();
|
void clearMessages();
|
||||||
void cancelRequest();
|
void cancelRequest();
|
||||||
|
|
||||||
|
Context::ContextManager *contextManager() const;
|
||||||
|
|
||||||
|
void setChatFilePath(const QString &filePath);
|
||||||
|
QString chatFilePath() 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 handleThinkingBlockReceived(
|
||||||
|
const QString &requestId, const QString &thinking, const QString &signature);
|
||||||
|
void handleToolExecutionStarted(
|
||||||
|
const QString &requestId, const QString &toolId, const QString &toolName);
|
||||||
|
void handleToolExecutionCompleted(
|
||||||
|
const QString &requestId,
|
||||||
|
const QString &toolId,
|
||||||
|
const QString &toolName,
|
||||||
|
const QString &toolOutput);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
|
void handleLLMResponse(const QString &response, const QJsonObject &request);
|
||||||
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;
|
||||||
|
bool isImageFile(const QString &filePath) const;
|
||||||
|
QString getMediaTypeForImage(const QString &filePath) const;
|
||||||
|
QString encodeImageToBase64(const QString &filePath) const;
|
||||||
|
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
|
||||||
|
|
||||||
LLMCore::RequestHandler *m_requestHandler;
|
struct RequestContext
|
||||||
|
{
|
||||||
|
QJsonObject originalRequest;
|
||||||
|
PluginLLMCore::Provider *provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
ChatModel *m_chatModel;
|
ChatModel *m_chatModel;
|
||||||
|
Context::ContextManager *m_contextManager;
|
||||||
|
QString m_chatFilePath;
|
||||||
|
|
||||||
|
QHash<QString, RequestContext> m_activeRequests;
|
||||||
|
QHash<QString, QString> m_accumulatedResponses;
|
||||||
|
QSet<QString> m_awaitingContinuation;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
60
ChatView/FileItem.cpp
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "FileItem.hpp"
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <logger/Logger.hpp>
|
||||||
|
#include <utils/filepath.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileItem::FileItem(QQuickItem *parent)
|
||||||
|
: QQuickItem(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void FileItem::openFileInEditor()
|
||||||
|
{
|
||||||
|
if (m_filePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::FilePath filePathObj = Utils::FilePath::fromString(m_filePath);
|
||||||
|
Core::IEditor *editor = Core::EditorManager::openEditor(filePathObj);
|
||||||
|
|
||||||
|
if (!editor) {
|
||||||
|
LOG_MESSAGE(QString("Failed to open file in editor: %1").arg(m_filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileItem::openFileInExternalEditor()
|
||||||
|
{
|
||||||
|
if (m_filePath.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool success = QDesktopServices::openUrl(QUrl::fromLocalFile(m_filePath));
|
||||||
|
if (success) {
|
||||||
|
LOG_MESSAGE(QString("Opened file in external application: %1").arg(m_filePath));
|
||||||
|
} else {
|
||||||
|
LOG_MESSAGE(QString("Failed to open file externally: %1").arg(m_filePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileItem::filePath() const
|
||||||
|
{
|
||||||
|
return m_filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileItem::setFilePath(const QString &newFilePath)
|
||||||
|
{
|
||||||
|
if (m_filePath == newFilePath)
|
||||||
|
return;
|
||||||
|
m_filePath = newFilePath;
|
||||||
|
emit filePathChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
32
ChatView/FileItem.hpp
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QQuickItem>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class FileItem: public QQuickItem
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_NAMED_ELEMENT(FileItem)
|
||||||
|
|
||||||
|
Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
FileItem(QQuickItem *parent = nullptr);
|
||||||
|
|
||||||
|
Q_INVOKABLE void openFileInEditor();
|
||||||
|
Q_INVOKABLE void openFileInExternalEditor();
|
||||||
|
|
||||||
|
QString filePath() const;
|
||||||
|
void setFilePath(const QString &newFilePath);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void filePathChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString m_filePath;
|
||||||
|
};
|
||||||
|
}
|
||||||
426
ChatView/FileMentionItem.cpp
Normal file
@@ -0,0 +1,426 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "FileMentionItem.hpp"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QTextStream>
|
||||||
|
|
||||||
|
#include <coreplugin/editormanager/documentmodel.h>
|
||||||
|
#include <coreplugin/editormanager/editormanager.h>
|
||||||
|
#include <projectexplorer/project.h>
|
||||||
|
#include <projectexplorer/projectmanager.h>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
FileMentionItem::FileMentionItem(QQuickItem *parent)
|
||||||
|
: QQuickItem(parent)
|
||||||
|
{}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchResults() const
|
||||||
|
{
|
||||||
|
return m_searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FileMentionItem::currentIndex() const
|
||||||
|
{
|
||||||
|
return m_currentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::setCurrentIndex(int index)
|
||||||
|
{
|
||||||
|
if (m_currentIndex == index)
|
||||||
|
return;
|
||||||
|
m_currentIndex = index;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::updateSearch(const QString &query)
|
||||||
|
{
|
||||||
|
m_lastQuery = query;
|
||||||
|
|
||||||
|
QVariantList openFiles = getOpenFiles(query);
|
||||||
|
QVariantList projectResults = searchProjectFiles(query);
|
||||||
|
|
||||||
|
QSet<QString> openPaths;
|
||||||
|
for (const QVariant &item : std::as_const(openFiles)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
openPaths.insert(map.value("absolutePath").toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList combined = openFiles;
|
||||||
|
for (const QVariant &item : std::as_const(projectResults)) {
|
||||||
|
const QVariantMap map = item.toMap();
|
||||||
|
if (!map.value("isProject").toBool()
|
||||||
|
&& openPaths.contains(map.value("absolutePath").toString()))
|
||||||
|
continue;
|
||||||
|
combined.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_searchResults = combined;
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::refreshSearch()
|
||||||
|
{
|
||||||
|
if (!m_lastQuery.isNull())
|
||||||
|
updateSearch(m_lastQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveUp()
|
||||||
|
{
|
||||||
|
if (m_currentIndex > 0) {
|
||||||
|
m_currentIndex--;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::moveDown()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < m_searchResults.size() - 1) {
|
||||||
|
m_currentIndex++;
|
||||||
|
emit currentIndexChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::selectCurrent()
|
||||||
|
{
|
||||||
|
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||||
|
if (item.value("isProject").toBool()) {
|
||||||
|
emit projectSelected(item.value("projectName").toString());
|
||||||
|
} else {
|
||||||
|
emit fileSelected(
|
||||||
|
item.value("absolutePath").toString(),
|
||||||
|
item.value("relativePath").toString(),
|
||||||
|
item.value("projectName").toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::dismiss()
|
||||||
|
{
|
||||||
|
m_searchResults.clear();
|
||||||
|
m_currentIndex = 0;
|
||||||
|
emit searchResultsChanged();
|
||||||
|
emit currentIndexChanged();
|
||||||
|
emit dismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap FileMentionItem::applyCurrentSelection(
|
||||||
|
const QString &text, int cursorPosition, bool useTools)
|
||||||
|
{
|
||||||
|
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size()) {
|
||||||
|
dismiss();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString textBefore = text.left(cursorPosition);
|
||||||
|
const int atIndex = textBefore.lastIndexOf('@');
|
||||||
|
if (atIndex < 0) {
|
||||||
|
dismiss();
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
|
||||||
|
QString replacement;
|
||||||
|
|
||||||
|
if (item.value("isProject").toBool()) {
|
||||||
|
replacement = QStringLiteral("@") + item.value("projectName").toString() + ":";
|
||||||
|
} else {
|
||||||
|
const QString currentQuery = textBefore.mid(atIndex + 1);
|
||||||
|
const QVariantMap result = handleFileSelection(
|
||||||
|
item.value("absolutePath").toString(),
|
||||||
|
item.value("relativePath").toString(),
|
||||||
|
item.value("projectName").toString(),
|
||||||
|
currentQuery,
|
||||||
|
useTools);
|
||||||
|
|
||||||
|
if (result.value("mode").toString() == "mention")
|
||||||
|
replacement = result.value("mentionText").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString newText = text.left(atIndex) + replacement + text.mid(cursorPosition);
|
||||||
|
const int newCursorPosition = atIndex + replacement.length();
|
||||||
|
|
||||||
|
dismiss();
|
||||||
|
|
||||||
|
return {{"text", newText}, {"cursorPosition", newCursorPosition}};
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantMap FileMentionItem::handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools)
|
||||||
|
{
|
||||||
|
QVariantMap result;
|
||||||
|
const QString fileName = relativePath.section('/', -1);
|
||||||
|
|
||||||
|
QString mentionKey = fileName;
|
||||||
|
const int colonIdx = currentQuery.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString projPrefix = currentQuery.left(colonIdx);
|
||||||
|
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
|
||||||
|
mentionKey = projPrefix + ":" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useTools) {
|
||||||
|
registerMention(mentionKey, absolutePath);
|
||||||
|
result["mode"] = QStringLiteral("mention");
|
||||||
|
result["mentionText"] = "@" + mentionKey + " ";
|
||||||
|
} else {
|
||||||
|
emit fileAttachRequested({absolutePath});
|
||||||
|
result["mode"] = QStringLiteral("attach");
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
|
||||||
|
{
|
||||||
|
m_atMentionMap[mentionKey] = absolutePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileMentionItem::clearMentions()
|
||||||
|
{
|
||||||
|
m_atMentionMap.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::expandMentions(const QString &text)
|
||||||
|
{
|
||||||
|
QString result = text;
|
||||||
|
|
||||||
|
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
|
||||||
|
const QString &mentionKey = it.key();
|
||||||
|
const QString &absPath = it.value();
|
||||||
|
const QString displayName = mentionKey.section(':', -1);
|
||||||
|
const QString escaped = QRegularExpression::escape(mentionKey);
|
||||||
|
|
||||||
|
// @key:N-M -> hyperlink + inline code block
|
||||||
|
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
|
||||||
|
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
|
||||||
|
QList<QRegularExpressionMatch> matches;
|
||||||
|
while (matchIt.hasNext())
|
||||||
|
matches.append(matchIt.next());
|
||||||
|
|
||||||
|
for (int i = matches.size() - 1; i >= 0; --i) {
|
||||||
|
const auto &m = matches[i];
|
||||||
|
const int startLine = m.captured(1).toInt();
|
||||||
|
const int endLine = m.captured(2).toInt();
|
||||||
|
const QString ext = fileExtension(absPath);
|
||||||
|
const QString snippet = readFileLines(absPath, startLine, endLine);
|
||||||
|
const QString replacement
|
||||||
|
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
|
||||||
|
.arg(displayName)
|
||||||
|
.arg(startLine)
|
||||||
|
.arg(endLine)
|
||||||
|
.arg(absPath, ext, snippet);
|
||||||
|
result.replace(m.capturedStart(), m.capturedLength(), replacement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @key -> hyperlink only
|
||||||
|
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
|
||||||
|
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
|
||||||
|
struct FileResult
|
||||||
|
{
|
||||||
|
QString absolutePath;
|
||||||
|
QString relativePath;
|
||||||
|
QString projectName;
|
||||||
|
int priority;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto allProjects = ProjectExplorer::ProjectManager::projects();
|
||||||
|
|
||||||
|
QString projectFilter;
|
||||||
|
QString fileQuery = query;
|
||||||
|
const int colonIdx = query.indexOf(':');
|
||||||
|
if (colonIdx > 0) {
|
||||||
|
const QString prefix = query.left(colonIdx);
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
|
||||||
|
projectFilter = project->displayName();
|
||||||
|
fileQuery = query.mid(colonIdx + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectFilter.isEmpty() && colonIdx < 0) {
|
||||||
|
const QString lowerQ = query.toLower();
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
const QString name = project->displayName();
|
||||||
|
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = QString();
|
||||||
|
item["relativePath"] = name;
|
||||||
|
item["projectName"] = name;
|
||||||
|
item["isProject"] = true;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<FileResult> candidates;
|
||||||
|
const QString lowerFileQuery = fileQuery.toLower();
|
||||||
|
const bool emptyFileQuery = fileQuery.isEmpty();
|
||||||
|
|
||||||
|
for (auto project : allProjects) {
|
||||||
|
if (!project)
|
||||||
|
continue;
|
||||||
|
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
|
||||||
|
const QString projectDir = project->projectDirectory().path();
|
||||||
|
const QString projectName = project->displayName();
|
||||||
|
|
||||||
|
for (const auto &filePath : projectFiles) {
|
||||||
|
const QString absolutePath = filePath.path();
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
|
||||||
|
int priority = -1;
|
||||||
|
if (emptyFileQuery) {
|
||||||
|
priority = 3;
|
||||||
|
} else if (lowerFileName == lowerFileQuery) {
|
||||||
|
priority = 0;
|
||||||
|
} else if (lowerFileName.startsWith(lowerFileQuery)) {
|
||||||
|
priority = 1;
|
||||||
|
} else if (lowerFileName.contains(lowerFileQuery)) {
|
||||||
|
priority = 2;
|
||||||
|
} else if (lowerRelativePath.contains(lowerFileQuery)) {
|
||||||
|
priority = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priority >= 0)
|
||||||
|
candidates.append({absolutePath, relativePath, projectName, priority});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
|
||||||
|
if (a.priority != b.priority)
|
||||||
|
return a.priority < b.priority;
|
||||||
|
return a.relativePath < b.relativePath;
|
||||||
|
});
|
||||||
|
|
||||||
|
const int maxFiles = qMax(0, 10 - results.size());
|
||||||
|
const int count = qMin(candidates.size(), maxFiles);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = candidates[i].absolutePath;
|
||||||
|
item["relativePath"] = candidates[i].relativePath;
|
||||||
|
item["projectName"] = candidates[i].projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
results.append(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariantList FileMentionItem::getOpenFiles(const QString &query)
|
||||||
|
{
|
||||||
|
QVariantList results;
|
||||||
|
const QString lowerQuery = query.toLower();
|
||||||
|
const bool emptyQuery = query.isEmpty();
|
||||||
|
QSet<QString> addedPaths;
|
||||||
|
|
||||||
|
auto tryAddDocument = [&](Core::IDocument *document) {
|
||||||
|
if (!document)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString absolutePath = document->filePath().toFSPathString();
|
||||||
|
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QFileInfo fileInfo(absolutePath);
|
||||||
|
const QString fileName = fileInfo.fileName();
|
||||||
|
if (fileName.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
QString relativePath = absolutePath;
|
||||||
|
QString projectName;
|
||||||
|
|
||||||
|
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
|
||||||
|
if (project) {
|
||||||
|
projectName = project->displayName();
|
||||||
|
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!emptyQuery) {
|
||||||
|
const QString lowerFileName = fileName.toLower();
|
||||||
|
const QString lowerRelativePath = relativePath.toLower();
|
||||||
|
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addedPaths.insert(absolutePath);
|
||||||
|
|
||||||
|
QVariantMap item;
|
||||||
|
item["absolutePath"] = absolutePath;
|
||||||
|
item["relativePath"] = relativePath;
|
||||||
|
item["projectName"] = projectName;
|
||||||
|
item["isProject"] = false;
|
||||||
|
item["isOpen"] = true;
|
||||||
|
results.append(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auto current = Core::EditorManager::currentEditor())
|
||||||
|
tryAddDocument(current->document());
|
||||||
|
|
||||||
|
for (auto editor : Core::EditorManager::visibleEditors())
|
||||||
|
if (editor)
|
||||||
|
tryAddDocument(editor->document());
|
||||||
|
|
||||||
|
for (auto document : Core::DocumentModel::openedDocuments())
|
||||||
|
tryAddDocument(document);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
|
||||||
|
{
|
||||||
|
QFile file(filePath);
|
||||||
|
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QTextStream stream(&file);
|
||||||
|
QString result;
|
||||||
|
int lineNum = 1;
|
||||||
|
while (!stream.atEnd()) {
|
||||||
|
const QString line = stream.readLine();
|
||||||
|
if (lineNum >= startLine)
|
||||||
|
result += line + '\n';
|
||||||
|
if (lineNum >= endLine)
|
||||||
|
break;
|
||||||
|
++lineNum;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString FileMentionItem::fileExtension(const QString &filePath)
|
||||||
|
{
|
||||||
|
const int dot = filePath.lastIndexOf('.');
|
||||||
|
return dot >= 0 ? filePath.mid(dot + 1) : QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
70
ChatView/FileMentionItem.hpp
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QHash>
|
||||||
|
#include <QQuickItem>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QVariantList>
|
||||||
|
|
||||||
|
namespace QodeAssist::Chat {
|
||||||
|
|
||||||
|
class FileMentionItem : public QQuickItem
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
|
||||||
|
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
|
||||||
|
|
||||||
|
QML_ELEMENT
|
||||||
|
public:
|
||||||
|
explicit FileMentionItem(QQuickItem *parent = nullptr);
|
||||||
|
|
||||||
|
QVariantList searchResults() const;
|
||||||
|
int currentIndex() const;
|
||||||
|
void setCurrentIndex(int index);
|
||||||
|
|
||||||
|
Q_INVOKABLE void updateSearch(const QString &query);
|
||||||
|
Q_INVOKABLE void refreshSearch();
|
||||||
|
Q_INVOKABLE void moveUp();
|
||||||
|
Q_INVOKABLE void moveDown();
|
||||||
|
Q_INVOKABLE void selectCurrent();
|
||||||
|
Q_INVOKABLE void dismiss();
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap handleFileSelection(
|
||||||
|
const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName,
|
||||||
|
const QString ¤tQuery,
|
||||||
|
bool useTools);
|
||||||
|
|
||||||
|
Q_INVOKABLE QVariantMap applyCurrentSelection(
|
||||||
|
const QString &text, int cursorPosition, bool useTools);
|
||||||
|
|
||||||
|
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
|
||||||
|
Q_INVOKABLE void clearMentions();
|
||||||
|
Q_INVOKABLE QString expandMentions(const QString &text);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void searchResultsChanged();
|
||||||
|
void currentIndexChanged();
|
||||||
|
void fileSelected(const QString &absolutePath,
|
||||||
|
const QString &relativePath,
|
||||||
|
const QString &projectName);
|
||||||
|
void projectSelected(const QString &projectName);
|
||||||
|
void dismissed();
|
||||||
|
void fileAttachRequested(const QStringList &filePaths);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QVariantList searchProjectFiles(const QString &query);
|
||||||
|
QVariantList getOpenFiles(const QString &query);
|
||||||
|
QString readFileLines(const QString &filePath, int startLine, int endLine);
|
||||||
|
static QString fileExtension(const QString &filePath);
|
||||||
|
|
||||||
|
QVariantList m_searchResults;
|
||||||
|
int m_currentIndex = 0;
|
||||||
|
QString m_lastQuery;
|
||||||
|
QHash<QString, QString> m_atMentionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist::Chat
|
||||||
@@ -1,51 +1,30 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#include <qobject.h>
|
#include <QObject>
|
||||||
#include <qqmlintegration.h>
|
#include <QtQmlIntegration>
|
||||||
|
|
||||||
|
#include "ChatData.hpp"
|
||||||
|
|
||||||
namespace QodeAssist::Chat {
|
namespace QodeAssist::Chat {
|
||||||
Q_NAMESPACE
|
|
||||||
|
|
||||||
class MessagePart
|
class MessagePart
|
||||||
{
|
{
|
||||||
Q_GADGET
|
Q_GADGET
|
||||||
Q_PROPERTY(PartType type MEMBER type CONSTANT FINAL)
|
Q_PROPERTY(MessagePartType 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)
|
||||||
|
Q_PROPERTY(QString imageData MEMBER imageData CONSTANT FINAL)
|
||||||
|
Q_PROPERTY(QString mediaType MEMBER mediaType CONSTANT FINAL)
|
||||||
QML_VALUE_TYPE(messagePart)
|
QML_VALUE_TYPE(messagePart)
|
||||||
public:
|
public:
|
||||||
enum PartType { Code, Text };
|
MessagePartType type;
|
||||||
Q_ENUM(PartType)
|
|
||||||
|
|
||||||
PartType type;
|
|
||||||
QString text;
|
QString text;
|
||||||
QString language;
|
QString language;
|
||||||
|
QString imageData; // Base64 data or URL
|
||||||
|
QString mediaType; // e.g., "image/png", "image/jpeg"
|
||||||
};
|
};
|
||||||
|
|
||||||
class MessagePartType : public MessagePart
|
|
||||||
{
|
|
||||||
Q_GADGET
|
|
||||||
};
|
|
||||||
|
|
||||||
QML_NAMED_ELEMENT(MessagePart)
|
|
||||||
QML_FOREIGN_NAMESPACE(QodeAssist::Chat::MessagePartType)
|
|
||||||
} // namespace QodeAssist::Chat
|
} // namespace QodeAssist::Chat
|
||||||
|
|||||||
15
ChatView/icons/apply-changes-button.svg
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 548 B |
10
ChatView/icons/chat-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 523 B |
10
ChatView/icons/chat-pause-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 548 B |
8
ChatView/icons/clean-icon-dark.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 822 B |
10
ChatView/icons/compress-icon.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
|
||||||
|
<line x1="12" y1="3" x2="12" y2="10" />
|
||||||
|
<polyline points="9 7 12 10 15 7" />
|
||||||
|
|
||||||
|
<line x1="12" y1="21" x2="12" y2="14" />
|
||||||
|
<polyline points="9 17 12 14 15 17" />
|
||||||
|
|
||||||
|
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 487 B |
5
ChatView/icons/context-icon.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
|
||||||
|
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 233 B |
12
ChatView/icons/file-in-system.svg
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 624 B |
6
ChatView/icons/image-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||||
|
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||||
|
<polyline points="21 15 16 10 5 21"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 304 B |
5
ChatView/icons/load-chat-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 370 B |
17
ChatView/icons/open-in-editor.svg
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 943 B |
16
ChatView/icons/reject-changes-button.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 599 B |
9
ChatView/icons/rules-icon.svg
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
5
ChatView/icons/save-chat-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 370 B |
4
ChatView/icons/settings-icon.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 962 B |
4
ChatView/icons/thinking-icon-off.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<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" fill-opacity="0.6"/>
|
||||||
|
<path d="M6 35L38 6" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
3
ChatView/icons/thinking-icon-on.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
11
ChatView/icons/tools-icon-off.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_71)">
|
||||||
|
<path d="M10.7777 0.0435181C14.2316 -0.253961 17.6161 0.979215 20.0629 3.42633C23.4139 6.77767 24.3012 11.6719 22.7299 15.8433C22.9016 15.988 23.0706 16.1419 23.2377 16.3072L42.2221 34.2203C42.2288 34.2268 42.2353 34.2344 42.2426 34.2408C44.4752 36.4735 44.4752 40.1064 42.2426 42.3394C40.0096 44.5717 36.4035 44.5446 34.1713 42.3121C34.1617 42.3031 34.1528 42.2937 34.144 42.2838L16.3871 23.1519C16.2254 22.9894 16.0746 22.8196 15.933 22.6451C11.7604 24.2194 6.86327 23.3335 3.50919 19.98C1.06298 17.5327 -0.171482 14.1483 0.126373 10.6949C0.160109 10.3034 0.41818 9.96685 0.78653 9.83258C1.15602 9.69759 1.57009 9.78945 1.84805 10.067L7.53555 15.7535L13.8402 13.7574L15.8363 7.4527L10.1488 1.7652C9.87057 1.48716 9.77945 1.07345 9.91348 0.704651C10.0489 0.335072 10.3852 0.0774496 10.7777 0.0435181ZM37.3656 34.7496L37.3129 34.9302L37.1586 35.4673L36.8363 35.5679L36.6195 35.2047L36.4623 34.942L36.2357 35.148L35.725 35.6148L35.5746 35.7525L35.6791 35.9283L35.9184 36.3287L35.7104 36.6548L35.1742 36.5543L34.9408 36.5093L34.8852 36.7418L34.7572 37.275L34.7123 37.4644L34.8842 37.5543L35.3891 37.8179V38.1802L34.8842 38.4449L34.7123 38.5347L34.7572 38.7242L34.8852 39.2574L34.9408 39.4898L35.1742 39.4449L35.7104 39.3433L35.9184 39.6695L35.6791 40.0709L35.5746 40.2466L35.725 40.3843L36.2357 40.8511L36.4623 41.0572L36.6195 40.7945L36.8363 40.4302L37.1586 40.5308L37.3129 41.0689L37.3656 41.2496H38.6352L38.6879 41.0689L38.8412 40.5308L39.1635 40.4302L39.3813 40.7945L39.5385 41.0572L39.765 40.8511L40.2758 40.3843L40.4262 40.2466L40.3217 40.0709L40.0815 39.6695L40.2895 39.3433L40.8266 39.4449L41.06 39.4898L41.1156 39.2574L41.2436 38.7242L41.2885 38.5347L41.1166 38.4449L40.6117 38.1802V37.8179L41.1166 37.5543L41.2885 37.4644L41.2436 37.275L41.1156 36.7418L41.06 36.5093L40.8266 36.5543L40.2895 36.6548L40.0815 36.3287L40.3217 35.9283L40.4262 35.7525L40.2758 35.6148L39.765 35.148L39.5385 34.942L39.3813 35.2047L39.1635 35.5679L38.8412 35.4673L38.6879 34.9302L38.6352 34.7496H37.3656Z" fill="black" fill-opacity="0.6"/>
|
||||||
|
<path d="M6 36L38 7" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_71">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
10
ChatView/icons/tools-icon-on.svg
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_82_50)">
|
||||||
|
<path d="M10.7775 0.0441895C14.2315 -0.253375 17.6159 0.979824 20.0627 3.427C23.4135 6.77842 24.3011 11.6726 22.7297 15.844C22.9013 15.9886 23.0714 16.1416 23.2385 16.3069L42.2219 34.2209C42.2285 34.2274 42.2352 34.2341 42.2424 34.2405C44.475 36.4732 44.475 40.1061 42.2424 42.3391C40.0094 44.5715 36.4033 44.5444 34.1711 42.3118C34.1615 42.3028 34.1525 42.2934 34.1437 42.2834L16.3869 23.1516C16.2251 22.9891 16.0745 22.8193 15.9328 22.6448C11.7602 24.2191 6.86304 23.3333 3.50897 19.9797C1.06276 17.5324 -0.171773 14.148 0.12616 10.6946C0.159908 10.3029 0.418723 9.96644 0.787292 9.83228C1.15667 9.69748 1.56997 9.78926 1.84784 10.0667L7.53534 15.7532L13.84 13.7571L15.8361 7.45239L10.1486 1.76489C9.87052 1.48684 9.78022 1.07306 9.91425 0.704346C10.0498 0.334991 10.3852 0.0781082 10.7775 0.0441895ZM37.3654 34.7502L37.3127 34.9309L37.1584 35.468L36.8361 35.5686L36.6193 35.2053L36.4621 34.9426L36.2355 35.1487L35.7248 35.6155L35.5744 35.7532L35.6789 35.929L35.9182 36.3293L35.7101 36.6555L35.174 36.5549L34.9406 36.51L34.8849 36.7424L34.757 37.2756L34.7121 37.4651L34.884 37.5549L35.3889 37.8186V38.1809L34.884 38.4456L34.7121 38.5354L34.757 38.7249L34.8849 39.2581L34.9406 39.4905L35.174 39.4456L35.7101 39.344L35.9182 39.6702L35.6789 40.0715L35.5744 40.2473L35.7248 40.385L36.2355 40.8518L36.4621 41.0579L36.6193 40.7952L36.8361 40.4309L37.1584 40.5315L37.3127 41.0696L37.3654 41.2502H38.6349L38.6877 41.0696L38.841 40.5315L39.1633 40.4309L39.381 40.7952L39.5383 41.0579L39.7648 40.8518L40.2756 40.385L40.426 40.2473L40.3215 40.0715L40.0812 39.6702L40.2892 39.344L40.8264 39.4456L41.0598 39.4905L41.1154 39.2581L41.2433 38.7249L41.2883 38.5354L41.1164 38.4456L40.6115 38.1809V37.8186L41.1164 37.5549L41.2883 37.4651L41.2433 37.2756L41.1154 36.7424L41.0598 36.51L40.8264 36.5549L40.2892 36.6555L40.0812 36.3293L40.3215 35.929L40.426 35.7532L40.2756 35.6155L39.7648 35.1487L39.5383 34.9426L39.381 35.2053L39.1633 35.5686L38.841 35.468L38.6877 34.9309L38.6349 34.7502H37.3654Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_82_50">
|
||||||
|
<rect width="44" height="44" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
16
ChatView/icons/undo-changes-button.svg
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 663 B |
5
ChatView/icons/window-lock.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 552 B |
5
ChatView/icons/window-unlock.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
|
After Width: | Height: | Size: 559 B |
@@ -1,40 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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 text: badgeText.text
|
|
||||||
|
|
||||||
implicitWidth: badgeText.implicitWidth + root.radius
|
|
||||||
implicitHeight: badgeText.implicitHeight + 6
|
|
||||||
color: palette.button
|
|
||||||
radius: root.height / 2
|
|
||||||
border.color: palette.mid
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: badgeText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: palette.buttonText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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 ChatView
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import "./dialog"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property alias msgModel: msgCreator.model
|
|
||||||
property alias messageAttachments: attachmentsModel.model
|
|
||||||
property bool isUserMessage: false
|
|
||||||
|
|
||||||
height: msgColumn.implicitHeight + 10
|
|
||||||
radius: 8
|
|
||||||
color: isUserMessage ? palette.alternateBase
|
|
||||||
: palette.base
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: msgColumn
|
|
||||||
|
|
||||||
x: 5
|
|
||||||
width: parent.width - x
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: msgCreator
|
|
||||||
delegate: Loader {
|
|
||||||
id: msgCreatorDelegate
|
|
||||||
// Fix me:
|
|
||||||
// why does `required property MessagePart modelData` not work?
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
Layout.preferredWidth: root.width
|
|
||||||
sourceComponent: {
|
|
||||||
// If `required property MessagePart modelData` is used
|
|
||||||
// and conversion to MessagePart fails, you're left
|
|
||||||
// with a nullptr. This tests that to prevent crashing.
|
|
||||||
if(!modelData) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch(modelData.type) {
|
|
||||||
case MessagePart.Text: return textComponent;
|
|
||||||
case MessagePart.Code: return codeBlockComponent;
|
|
||||||
default: return textComponent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: textComponent
|
|
||||||
TextComponent {
|
|
||||||
itemData: msgCreatorDelegate.modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: codeBlockComponent
|
|
||||||
CodeBlockComponent {
|
|
||||||
itemData: msgCreatorDelegate.modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flow {
|
|
||||||
id: attachmentsFlow
|
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
visible: attachmentsModel.model && attachmentsModel.model.length > 0
|
|
||||||
leftPadding: 10
|
|
||||||
rightPadding: 10
|
|
||||||
spacing: 5
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
id: attachmentsModel
|
|
||||||
|
|
||||||
delegate: Rectangle {
|
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
height: attachText.implicitHeight + 8
|
|
||||||
width: attachText.implicitWidth + 16
|
|
||||||
radius: 4
|
|
||||||
color: palette.button
|
|
||||||
border.width: 1
|
|
||||||
border.color: palette.mid
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: attachText
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: modelData
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: userMessageMarker
|
|
||||||
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
width: 3
|
|
||||||
height: root.height - root.radius
|
|
||||||
color: "#92BD6C"
|
|
||||||
radius: root.radius
|
|
||||||
visible: root.isUserMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
component TextComponent : TextBlock {
|
|
||||||
required property var itemData
|
|
||||||
height: implicitHeight + 10
|
|
||||||
verticalAlignment: Text.AlignVCenter
|
|
||||||
leftPadding: 10
|
|
||||||
text: itemData.text
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
component CodeBlockComponent : CodeBlock {
|
|
||||||
required property var itemData
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 10
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 10
|
|
||||||
}
|
|
||||||
|
|
||||||
code: itemData.text
|
|
||||||
language: itemData.language
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +1,16 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
||||||
import QtQuick.Controls
|
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 Qt.labs.platform as Platform
|
||||||
|
|
||||||
|
import "./chatparts"
|
||||||
import "./controls"
|
import "./controls"
|
||||||
import "./parts"
|
|
||||||
|
|
||||||
ChatRootView {
|
ChatRootView {
|
||||||
id: root
|
id: root
|
||||||
@@ -56,7 +43,41 @@ ChatRootView {
|
|||||||
color: palette.window
|
color: palette.window
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SplitDropZone {
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 99
|
||||||
|
|
||||||
|
onFilesDroppedToAttach: (urlStrings) => {
|
||||||
|
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||||
|
if (localPaths.length > 0) {
|
||||||
|
root.addFilesToAttachList(localPaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onFilesDroppedToLink: (urlStrings) => {
|
||||||
|
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
|
||||||
|
if (localPaths.length > 0) {
|
||||||
|
root.addFilesToLinkList(localPaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoABusyOverlay {
|
||||||
|
id: compressingOverlay
|
||||||
|
|
||||||
|
z: 50
|
||||||
|
|
||||||
|
anchors.fill: mainColumn
|
||||||
|
anchors.topMargin: topBar.height
|
||||||
|
anchors.bottomMargin: bottomBar.height
|
||||||
|
|
||||||
|
active: root.isCompressing
|
||||||
|
text: qsTr("Compressing chat…")
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
id: mainColumn
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
spacing: 0
|
spacing: 0
|
||||||
|
|
||||||
@@ -64,39 +85,111 @@ ChatRootView {
|
|||||||
id: topBar
|
id: topBar
|
||||||
|
|
||||||
Layout.preferredWidth: parent.width
|
Layout.preferredWidth: parent.width
|
||||||
Layout.preferredHeight: 40
|
Layout.preferredHeight: childrenRect.height + 10
|
||||||
|
|
||||||
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("tokens:%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
|
||||||
}
|
}
|
||||||
recentPath {
|
recentPath {
|
||||||
text: qsTr("Latest chat file name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
|
||||||
}
|
}
|
||||||
openChatHistory.onClicked: root.openChatHistoryFolder()
|
openChatHistory.onClicked: root.openChatHistoryFolder()
|
||||||
|
contextButton.onClicked: contextViewer.open()
|
||||||
|
pinButton {
|
||||||
|
visible: typeof _chatview !== 'undefined'
|
||||||
|
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
|
||||||
|
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
|
||||||
|
}
|
||||||
|
toolsButton {
|
||||||
|
checked: root.useTools
|
||||||
|
onCheckedChanged: {
|
||||||
|
root.useTools = toolsButton.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thinkingMode {
|
||||||
|
checked: root.useThinking
|
||||||
|
enabled: root.isThinkingSupport
|
||||||
|
onCheckedChanged: {
|
||||||
|
root.useThinking = thinkingMode.checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settingsButton.onClicked: root.openSettings()
|
||||||
|
configSelector {
|
||||||
|
model: root.availableConfigurations
|
||||||
|
displayText: root.currentConfiguration
|
||||||
|
onActivated: function(index) {
|
||||||
|
if (index > 0) {
|
||||||
|
root.applyConfiguration(root.availableConfigurations[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.onAboutToShow: {
|
||||||
|
root.loadAvailableConfigurations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
roleSelector {
|
||||||
|
model: root.availableAgentRoles
|
||||||
|
displayText: root.currentAgentRole
|
||||||
|
onActivated: function(index) {
|
||||||
|
root.applyAgentRole(root.availableAgentRoles[index])
|
||||||
|
}
|
||||||
|
|
||||||
|
popup.onAboutToShow: {
|
||||||
|
root.loadAvailableAgentRoles()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ListView {
|
ListView {
|
||||||
id: chatListView
|
id: chatListView
|
||||||
|
|
||||||
|
property bool userScrolledUp: false
|
||||||
|
|
||||||
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: 10
|
spacing: 0
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
cacheBuffer: 2000
|
cacheBuffer: 2000
|
||||||
|
|
||||||
delegate: ChatItem {
|
onMovingChanged: {
|
||||||
|
if (moving) {
|
||||||
|
userScrolledUp = !atYEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onAtYEndChanged: {
|
||||||
|
if (atYEnd) {
|
||||||
|
userScrolledUp = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Loader {
|
||||||
|
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)
|
|
||||||
messageAttachments: model.attachments
|
sourceComponent: {
|
||||||
isUserMessage: model.roleType === ChatModel.User
|
if (model.roleType === ChatModel.Tool) {
|
||||||
|
return toolMessageComponent
|
||||||
|
} else if (model.roleType === ChatModel.FileEdit) {
|
||||||
|
return fileEditMessageComponent
|
||||||
|
} else if (model.roleType === ChatModel.Thinking) {
|
||||||
|
return thinkingMessageComponent
|
||||||
|
} else {
|
||||||
|
return chatItemComponent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
header: Item {
|
header: Item {
|
||||||
@@ -108,15 +201,138 @@ ChatRootView {
|
|||||||
id: scroll
|
id: scroll
|
||||||
}
|
}
|
||||||
|
|
||||||
onCountChanged: {
|
Rectangle {
|
||||||
|
id: scrollToBottomButton
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
bottom: parent.bottom
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
bottomMargin: 10
|
||||||
|
}
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: palette.button
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
visible: chatListView.userScrolledUp
|
||||||
|
opacity: 0.9
|
||||||
|
z: 100
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "▼"
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: palette.buttonText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
chatListView.userScrolledUp = false
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on visible {
|
||||||
|
enabled: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onCountChanged: {
|
||||||
|
if (!userScrolledUp) {
|
||||||
|
root.scrollToBottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onContentHeightChanged: {
|
onContentHeightChanged: {
|
||||||
if (atYEnd) {
|
if (!userScrolledUp && atYEnd) {
|
||||||
root.scrollToBottom()
|
root.scrollToBottom()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: chatItemComponent
|
||||||
|
|
||||||
|
ChatItem {
|
||||||
|
id: chatItemInstance
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
msgModel: root.chatModel.processMessageContent(model.content)
|
||||||
|
messageAttachments: model.attachments
|
||||||
|
messageImages: model.images
|
||||||
|
chatFilePath: root.chatFilePath()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpenFileRequested: function(filePath) {
|
||||||
|
root.openFileInEditor(filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: toolMessageComponent
|
||||||
|
|
||||||
|
ToolBlock {
|
||||||
|
width: parent.width
|
||||||
|
toolContent: model.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: fileEditMessageComponent
|
||||||
|
|
||||||
|
FileEditBlock {
|
||||||
|
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
|
||||||
|
|
||||||
|
ThinkingBlock {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -129,7 +345,9 @@ ChatRootView {
|
|||||||
QQC.TextArea {
|
QQC.TextArea {
|
||||||
id: messageInput
|
id: messageInput
|
||||||
|
|
||||||
placeholderText: qsTr("Type your message here...")
|
placeholderText: Qt.platform.os === "osx"
|
||||||
|
? 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 {
|
||||||
@@ -150,15 +368,84 @@ ChatRootView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
|
onTextChanged: {
|
||||||
|
root.calculateMessageTokensCount(messageInput.text)
|
||||||
|
var cursorPos = messageInput.cursorPosition
|
||||||
|
var textBefore = messageInput.text.substring(0, cursorPos)
|
||||||
|
var atIndex = textBefore.lastIndexOf('@')
|
||||||
|
if (atIndex >= 0) {
|
||||||
|
var query = textBefore.substring(atIndex + 1)
|
||||||
|
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
|
||||||
|
fileMentionPopup.updateSearch(query)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileMentionPopup.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
|
if (fileMentionPopup.visible) {
|
||||||
root.sendChatMessage()
|
if (event.key === Qt.Key_Down) {
|
||||||
event.accepted = true;
|
fileMentionPopup.moveDown()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Up) {
|
||||||
|
fileMentionPopup.moveUp()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||||
|
root.applyMentionSelection()
|
||||||
|
event.accepted = true
|
||||||
|
} else if (event.key === Qt.Key_Escape) {
|
||||||
|
fileMentionPopup.dismiss()
|
||||||
|
event.accepted = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onClicked: messageContextMenu.open()
|
||||||
|
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 {
|
||||||
@@ -183,25 +470,56 @@ 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.sendChatMessage()
|
isCompressing: root.isCompressing
|
||||||
stopButton.onClicked: root.cancelRequest()
|
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
|
||||||
|
: root.cancelRequest()
|
||||||
|
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
|
||||||
|
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
|
||||||
|
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
|
||||||
|
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
|
||||||
|
: qsTr("Stop")
|
||||||
|
compressButton.onClicked: compressConfirmDialog.open()
|
||||||
|
cancelCompressButton.onClicked: root.cancelCompression()
|
||||||
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()
|
||||||
|
attachImages.onClicked: root.showAddImageDialog()
|
||||||
linkFiles.onClicked: root.showLinkFilesDialog()
|
linkFiles.onClicked: root.showLinkFilesDialog()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
id: sendMessageShortcut
|
||||||
|
|
||||||
|
sequences: ["Ctrl+Return", "Ctrl+Enter"]
|
||||||
|
context: Qt.WindowShortcut
|
||||||
|
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
|
||||||
|
onActivated: root.sendChatMessage()
|
||||||
|
}
|
||||||
|
|
||||||
function clearChat() {
|
function clearChat() {
|
||||||
root.chatModel.clear()
|
root.clearMessages()
|
||||||
root.clearAttachmentFiles()
|
root.clearAttachmentFiles()
|
||||||
root.updateInputTokensCount()
|
root.updateInputTokensCount()
|
||||||
}
|
}
|
||||||
@@ -210,9 +528,115 @@ ChatRootView {
|
|||||||
Qt.callLater(chatListView.positionViewAtEnd)
|
Qt.callLater(chatListView.positionViewAtEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyMentionSelection() {
|
||||||
|
var result = fileMentionPopup.applyCurrentSelection(
|
||||||
|
messageInput.text, messageInput.cursorPosition, root.useTools)
|
||||||
|
if (result.text !== undefined) {
|
||||||
|
messageInput.text = result.text
|
||||||
|
messageInput.cursorPosition = result.cursorPosition
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function sendChatMessage() {
|
function sendChatMessage() {
|
||||||
root.sendMessage(messageInput.text)
|
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
|
||||||
messageInput.text = ""
|
messageInput.text = ""
|
||||||
|
fileMentionPopup.clearMentions()
|
||||||
scrollToBottom()
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Dialog {
|
||||||
|
id: compressConfirmDialog
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
title: qsTr("Compress Chat")
|
||||||
|
modal: true
|
||||||
|
standardButtons: Dialog.Yes | Dialog.No
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: root.compressCurrentChat()
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
ContextViewer {
|
||||||
|
id: contextViewer
|
||||||
|
|
||||||
|
width: Math.min(parent.width * 0.85, 800)
|
||||||
|
height: Math.min(parent.height * 0.85, 700)
|
||||||
|
x: (parent.width - width) / 2
|
||||||
|
y: (parent.height - height) / 2
|
||||||
|
|
||||||
|
baseSystemPrompt: root.baseSystemPrompt
|
||||||
|
currentAgentRole: root.currentAgentRole
|
||||||
|
currentAgentRoleDescription: root.currentAgentRoleDescription
|
||||||
|
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
|
||||||
|
activeRules: root.activeRules
|
||||||
|
activeRulesCount: root.activeRulesCount
|
||||||
|
|
||||||
|
onOpenSettings: root.openSettings()
|
||||||
|
onOpenAgentRolesSettings: root.openAgentRolesSettings()
|
||||||
|
onOpenRulesFolder: root.openRulesFolder()
|
||||||
|
onRefreshRules: root.refreshRules()
|
||||||
|
onRuleSelected: function(index) {
|
||||||
|
contextViewer.selectedRuleContent = root.getRuleContent(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onOpenFilesChanged() {
|
||||||
|
if (fileMentionPopup.visible)
|
||||||
|
Qt.callLater(fileMentionPopup.refreshSearch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FileMentionPopup {
|
||||||
|
id: fileMentionPopup
|
||||||
|
|
||||||
|
z: 999
|
||||||
|
width: Math.min(480, root.width - 20)
|
||||||
|
|
||||||
|
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
|
||||||
|
y: view.y - height - 4
|
||||||
|
|
||||||
|
onSelectionRequested: root.applyMentionSelection()
|
||||||
|
|
||||||
|
onFileAttachRequested: function(filePaths) {
|
||||||
|
root.addFilesToAttachList(filePaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
messageInput.forceActiveFocus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
367
ChatView/qml/chatparts/ChatItem.qml
Normal file
@@ -0,0 +1,367 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import ChatView
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias msgModel: msgCreator.model
|
||||||
|
property alias messageAttachments: attachmentsModel.model
|
||||||
|
property alias messageImages: imagesModel.model
|
||||||
|
property string chatFilePath: ""
|
||||||
|
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)
|
||||||
|
signal openFileRequested(string filePath)
|
||||||
|
|
||||||
|
height: msgColumn.implicitHeight + 10
|
||||||
|
radius: 8
|
||||||
|
color: isUserMessage ? palette.alternateBase
|
||||||
|
: palette.base
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: mouse
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: msgColumn
|
||||||
|
|
||||||
|
x: 5
|
||||||
|
width: parent.width - x
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: msgCreator
|
||||||
|
delegate: Loader {
|
||||||
|
id: msgCreatorDelegate
|
||||||
|
// Fix me:
|
||||||
|
// why does `required property MessagePart modelData` not work?
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.preferredWidth: root.width
|
||||||
|
sourceComponent: {
|
||||||
|
// If `required property MessagePart modelData` is used
|
||||||
|
// and conversion to MessagePart fails, you're left
|
||||||
|
// with a nullptr. This tests that to prevent crashing.
|
||||||
|
if(!modelData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(modelData.type) {
|
||||||
|
case MessagePartType.Text: return textComponent;
|
||||||
|
case MessagePartType.Code: return codeBlockComponent;
|
||||||
|
default: return textComponent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: textComponent
|
||||||
|
TextComponent {
|
||||||
|
itemData: msgCreatorDelegate.modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: codeBlockComponent
|
||||||
|
CodeBlockComponent {
|
||||||
|
itemData: msgCreatorDelegate.modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: attachmentsFlow
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: attachmentsModel.model && attachmentsModel.model.length > 0
|
||||||
|
leftPadding: 10
|
||||||
|
rightPadding: 10
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: attachmentsModel
|
||||||
|
|
||||||
|
delegate: AttachmentComponent {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
itemData: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
id: imagesFlow
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: imagesModel.model && imagesModel.model.length > 0
|
||||||
|
leftPadding: 10
|
||||||
|
rightPadding: 10
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: imagesModel
|
||||||
|
|
||||||
|
delegate: ImageComponent {
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
itemData: modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: stopButtonId.hovered
|
||||||
|
text: qsTr("Reset chat to this message and edit")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component TextComponent : TextBlock {
|
||||||
|
required property var itemData
|
||||||
|
height: implicitHeight + 10
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
leftPadding: 10
|
||||||
|
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
|
||||||
|
: itemData.text
|
||||||
|
font.family: root.textFontFamily
|
||||||
|
font.pointSize: root.textFontSize
|
||||||
|
textFormat: {
|
||||||
|
if (root.textFormat == 0) {
|
||||||
|
return Text.MarkdownText
|
||||||
|
} else if (root.textFormat == 1) {
|
||||||
|
return Text.RichText
|
||||||
|
} else {
|
||||||
|
return Text.PlainText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLinkActivated: function(link) {
|
||||||
|
if (link.startsWith("file://")) {
|
||||||
|
var filePath = link.replace(/^file:\/\//, "")
|
||||||
|
root.openFileRequested(filePath)
|
||||||
|
} else {
|
||||||
|
Qt.openUrlExternally(link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUtils {
|
||||||
|
id: utils
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CodeBlockComponent : CodeBlock {
|
||||||
|
id: codeblock
|
||||||
|
|
||||||
|
required property var itemData
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 10
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 10
|
||||||
|
}
|
||||||
|
|
||||||
|
code: itemData.text
|
||||||
|
language: itemData.language
|
||||||
|
codeFontFamily: root.codeFontFamily
|
||||||
|
codeFontSize: root.codeFontSize
|
||||||
|
}
|
||||||
|
|
||||||
|
component AttachmentComponent : Rectangle {
|
||||||
|
required property var itemData
|
||||||
|
|
||||||
|
height: attachFileText.implicitHeight + 8
|
||||||
|
width: attachFileText.implicitWidth + 16
|
||||||
|
radius: 4
|
||||||
|
color: attachFileMouseArea.containsMouse ? Qt.lighter(palette.button, 1.1) : palette.button
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: fileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: attachFileText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: (itemData.fileName || "")
|
||||||
|
color: palette.buttonText
|
||||||
|
font.pointSize: root.textFontSize - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: attachFileMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
fileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
fileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: attachFileMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ImageComponent : Rectangle {
|
||||||
|
required property var itemData
|
||||||
|
|
||||||
|
readonly property int maxImageWidth: Math.min(400, root.width - 40)
|
||||||
|
readonly property int maxImageHeight: 300
|
||||||
|
|
||||||
|
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
|
||||||
|
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
|
||||||
|
radius: 4
|
||||||
|
color: imageMouseArea.containsMouse ? Qt.lighter(palette.base, 1.05) : palette.base
|
||||||
|
border.width: 1
|
||||||
|
border.color: palette.mid
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 100 } }
|
||||||
|
|
||||||
|
FileItem {
|
||||||
|
id: imageFileItem
|
||||||
|
filePath: itemData.filePath || ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 4
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: imageDisplay
|
||||||
|
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.maximumWidth: parent.parent.maxImageWidth
|
||||||
|
Layout.maximumHeight: parent.parent.maxImageHeight
|
||||||
|
|
||||||
|
source: itemData.imageUrl ? itemData.imageUrl : ""
|
||||||
|
|
||||||
|
sourceSize.width: parent.parent.maxImageWidth
|
||||||
|
sourceSize.height: parent.parent.maxImageHeight
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
cache: true
|
||||||
|
asynchronous: true
|
||||||
|
smooth: true
|
||||||
|
mipmap: true
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
running: imageDisplay.status === Image.Loading
|
||||||
|
visible: running
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Failed to load image")
|
||||||
|
visible: imageDisplay.status === Image.Error
|
||||||
|
color: palette.placeholderText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: fileNameText
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: itemData.fileName || ""
|
||||||
|
color: palette.text
|
||||||
|
font.pointSize: root.textFontSize - 1
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: imageMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
imageFileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
imageFileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: imageMouseArea.containsMouse
|
||||||
|
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
|
||||||
|
delay: 500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
157
ChatView/qml/chatparts/CodeBlock.qml
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
import Qt.labs.platform as Platform
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string code: ""
|
||||||
|
property string language: ""
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
property alias codeFontFamily: codeText.font.family
|
||||||
|
property alias codeFontSize: codeText.font.pointSize
|
||||||
|
readonly property real collapsedHeight: copyButton.height + 10
|
||||||
|
|
||||||
|
color: palette.alternateBase
|
||||||
|
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
||||||
|
: Qt.lighter(root.color, 1.3)
|
||||||
|
border.width: 2
|
||||||
|
radius: 4
|
||||||
|
implicitWidth: parent.width
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on implicitHeight {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatUtils {
|
||||||
|
id: utils
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hoverHandler
|
||||||
|
enabled: true
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: header
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: root.collapsedHeight
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.expanded = !root.expanded
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 10
|
||||||
|
}
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.language ? qsTr("Code (%1)").arg(root.language) :
|
||||||
|
qsTr("Code")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: root.expanded ? "▼" : "▶"
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: codeText
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
top: header.bottom
|
||||||
|
margins: 10
|
||||||
|
}
|
||||||
|
text: root.code
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
selectionColor: palette.highlight
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.RightButton
|
||||||
|
onClicked: contextMenu.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.Menu {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
Platform.MenuItem {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
onTriggered: {
|
||||||
|
const textToCopy = codeText.selectedText || root.code
|
||||||
|
utils.copyToClipboard(textToCopy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Platform.MenuSeparator {}
|
||||||
|
|
||||||
|
Platform.MenuItem {
|
||||||
|
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
|
||||||
|
onTriggered: root.expanded = !root.expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: copyButton
|
||||||
|
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 5
|
||||||
|
|
||||||
|
y: 5
|
||||||
|
text: qsTr("Copy")
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
utils.copyToClipboard(root.code)
|
||||||
|
text = qsTr("Copied")
|
||||||
|
copyTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: copyTimer
|
||||||
|
interval: 2000
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
453
ChatView/qml/chatparts/FileEditBlock.qml
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
ChatView/qml/chatparts/TextBlock.qml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Qt.labs.platform as Platform
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
selectionColor: palette.highlight
|
||||||
|
color: palette.text
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
ChatView/qml/chatparts/ThinkingBlock.qml
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
146
ChatView/qml/chatparts/ToolBlock.qml
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,26 +1,11 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
||||||
import QtQuick.Controls
|
import QtQuick.Controls
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import ChatView
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
Flow {
|
Flow {
|
||||||
id: root
|
id: root
|
||||||
@@ -40,19 +25,70 @@ Flow {
|
|||||||
Repeater {
|
Repeater {
|
||||||
id: attachRepeater
|
id: attachRepeater
|
||||||
|
|
||||||
delegate: Rectangle {
|
delegate: FileItem {
|
||||||
|
id: fileItem
|
||||||
|
|
||||||
required property int index
|
required property int index
|
||||||
required property string modelData
|
required property string modelData
|
||||||
|
|
||||||
|
filePath: modelData
|
||||||
|
|
||||||
height: 30
|
height: 30
|
||||||
width: contentRow.width + 10
|
width: contentRow.width + 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
radius: 4
|
radius: 4
|
||||||
color: palette.button
|
color: palette.button
|
||||||
border.width: 1
|
border.width: 1
|
||||||
border.color: mouse.hovered ? palette.highlight : root.accentColor
|
border.color: mouse.containsMouse ? palette.highlight : root.accentColor
|
||||||
|
}
|
||||||
|
|
||||||
HoverHandler {
|
MouseArea {
|
||||||
id: mouse
|
id: mouse
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (mouse.button === Qt.RightButton) {
|
||||||
|
contextMenu.popup()
|
||||||
|
} else if (mouse.button === Qt.MiddleButton ||
|
||||||
|
(mouse.button === Qt.LeftButton && (mouse.modifiers & Qt.ControlModifier))) {
|
||||||
|
root.removeFileFromListByIndex(fileItem.index)
|
||||||
|
} else if (mouse.modifiers & Qt.ShiftModifier) {
|
||||||
|
fileItem.openFileInExternalEditor()
|
||||||
|
} else {
|
||||||
|
fileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAToolTip {
|
||||||
|
visible: mouse.containsMouse
|
||||||
|
delay: 500
|
||||||
|
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: contextMenu
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Open in Qt Creator"
|
||||||
|
onTriggered: fileItem.openFileInEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Open in External Editor"
|
||||||
|
onTriggered: fileItem.openFileInExternalEditor()
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuSeparator {}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Remove"
|
||||||
|
onTriggered: root.removeFileFromListByIndex(fileItem.index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
153
ChatView/qml/controls/BottomBar.qml
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias sendButton: sendButtonId
|
||||||
|
property alias syncOpenFiles: syncOpenFilesId
|
||||||
|
property alias attachFiles: attachFilesId
|
||||||
|
property alias attachImages: attachImagesId
|
||||||
|
property alias linkFiles: linkFilesId
|
||||||
|
property alias compressButton: compressButtonId
|
||||||
|
property alias cancelCompressButton: cancelCompressButtonId
|
||||||
|
|
||||||
|
property bool isCompressing: false
|
||||||
|
|
||||||
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
|
Qt.darker(palette.window, 1.1) :
|
||||||
|
Qt.lighter(palette.window, 1.1)
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
id: bottomBar
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 5
|
||||||
|
right: parent.right
|
||||||
|
rightMargin: 5
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: attachFilesId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Attach file to message")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: attachImagesId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/image-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Attach image to message")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: linkFilesId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Link file to context")
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
id: syncOpenFilesId
|
||||||
|
|
||||||
|
text: qsTr("Sync open files")
|
||||||
|
|
||||||
|
ToolTip.visible: syncOpenFilesId.hovered
|
||||||
|
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: compressingRow
|
||||||
|
|
||||||
|
visible: root.isCompressing
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
BusyIndicator {
|
||||||
|
id: compressBusyIndicator
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
running: root.isCompressing
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Compressing...")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: palette.text
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: cancelCompressButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: qsTr("Cancel")
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Cancel compression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: compressButtonId
|
||||||
|
|
||||||
|
visible: !root.isCompressing
|
||||||
|
text: qsTr("Compress")
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: sendButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
542
ChatView/qml/controls/ContextViewer.qml
Normal file
@@ -0,0 +1,542 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls.Basic as QQC
|
||||||
|
|
||||||
|
import UIControls
|
||||||
|
import ChatView
|
||||||
|
|
||||||
|
Popup {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string baseSystemPrompt
|
||||||
|
property string currentAgentRole
|
||||||
|
property string currentAgentRoleDescription
|
||||||
|
property string currentAgentRoleSystemPrompt
|
||||||
|
property var activeRules
|
||||||
|
property int activeRulesCount
|
||||||
|
property string selectedRuleContent
|
||||||
|
|
||||||
|
signal openSettings()
|
||||||
|
signal openAgentRolesSettings()
|
||||||
|
signal openRulesFolder()
|
||||||
|
signal refreshRules()
|
||||||
|
signal ruleSelected(int index)
|
||||||
|
|
||||||
|
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: 8
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Chat Context")
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Refresh")
|
||||||
|
onClicked: root.refreshRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Close")
|
||||||
|
onClicked: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: mainFlickable
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
contentHeight: sectionsColumn.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: sectionsColumn
|
||||||
|
|
||||||
|
width: mainFlickable.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: systemPromptSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Base System Prompt")
|
||||||
|
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
|
||||||
|
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 5
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: systemPromptFlickable
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
contentHeight: systemPromptText.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: systemPromptText
|
||||||
|
|
||||||
|
width: systemPromptFlickable.width
|
||||||
|
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.baseSystemPrompt.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Edit in Settings")
|
||||||
|
onClicked: {
|
||||||
|
root.openSettings()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: agentRoleSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Agent Role")
|
||||||
|
badge: root.currentAgentRole
|
||||||
|
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.currentAgentRoleDescription
|
||||||
|
font.pixelSize: 11
|
||||||
|
font.italic: true
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.currentAgentRoleDescription.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
|
||||||
|
color: palette.base
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
visible: root.currentAgentRoleSystemPrompt.length > 0
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: agentPromptFlickable
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
contentHeight: agentPromptText.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: agentPromptText
|
||||||
|
|
||||||
|
width: agentPromptFlickable.width
|
||||||
|
text: root.currentAgentRoleSystemPrompt
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
color: palette.text
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("No role selected. Using base system prompt only.")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.currentAgentRoleSystemPrompt.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.currentAgentRoleSystemPrompt.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Manage Roles")
|
||||||
|
onClicked: {
|
||||||
|
root.openAgentRolesSettings()
|
||||||
|
root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CollapsibleSection {
|
||||||
|
id: projectRulesSection
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
title: qsTr("Project Rules")
|
||||||
|
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
|
||||||
|
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
|
||||||
|
|
||||||
|
sectionContent: ColumnLayout {
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
SplitView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 220
|
||||||
|
orientation: Qt.Horizontal
|
||||||
|
visible: root.activeRulesCount > 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
SplitView.minimumWidth: 120
|
||||||
|
SplitView.preferredWidth: 180
|
||||||
|
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 (%1)").arg(rulesList.count)
|
||||||
|
font.pixelSize: 11
|
||||||
|
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
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: ListView.view.width
|
||||||
|
height: ruleItemContent.implicitHeight + 8
|
||||||
|
highlighted: ListView.isCurrentItem
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: {
|
||||||
|
if (parent.highlighted)
|
||||||
|
return palette.highlight
|
||||||
|
if (parent.hovered)
|
||||||
|
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
|
||||||
|
return "transparent"
|
||||||
|
}
|
||||||
|
radius: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: ColumnLayout {
|
||||||
|
id: ruleItemContent
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.fileName
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: parent.parent.highlighted ? palette.highlightedText : palette.text
|
||||||
|
elide: Text.ElideMiddle
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.category
|
||||||
|
font.pixelSize: 9
|
||||||
|
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
rulesList.currentIndex = index
|
||||||
|
root.ruleSelected(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
SplitView.fillWidth: true
|
||||||
|
SplitView.minimumWidth: 200
|
||||||
|
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: 11
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Copy")
|
||||||
|
enabled: root.selectedRuleContent.length > 0
|
||||||
|
onClicked: utils.copyToClipboard(root.selectedRuleContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: ruleContentFlickable
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
contentHeight: ruleContentArea.implicitHeight
|
||||||
|
clip: true
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
|
||||||
|
TextEdit {
|
||||||
|
id: ruleContentArea
|
||||||
|
|
||||||
|
width: ruleContentFlickable.width
|
||||||
|
text: root.selectedRuleContent
|
||||||
|
readOnly: true
|
||||||
|
selectByMouse: true
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
selectionColor: palette.highlight
|
||||||
|
color: palette.text
|
||||||
|
font.family: "monospace"
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
|
||||||
|
font.pixelSize: 11
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
Layout.fillWidth: true
|
||||||
|
visible: root.activeRulesCount === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
|
||||||
|
Item { Layout.fillWidth: true }
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
text: qsTr("Open Rules Folder")
|
||||||
|
onClicked: root.openRulesFolder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QQC.ScrollBar.vertical: QQC.ScrollBar {
|
||||||
|
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
height: 1
|
||||||
|
color: palette.mid
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
|
||||||
|
font.pixelSize: 9
|
||||||
|
color: palette.mid
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CollapsibleSection: ColumnLayout {
|
||||||
|
id: sectionRoot
|
||||||
|
|
||||||
|
property string title
|
||||||
|
property string badge
|
||||||
|
property color badgeColor: palette.mid
|
||||||
|
property Component sectionContent: null
|
||||||
|
property bool expanded: false
|
||||||
|
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredHeight: 32
|
||||||
|
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 2
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sectionMouseArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: sectionRoot.expanded = !sectionRoot.expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 8
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: sectionRoot.expanded ? "▼" : "▶"
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: sectionRoot.title
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.bold: true
|
||||||
|
color: palette.text
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitWidth: badgeText.implicitWidth + 12
|
||||||
|
implicitHeight: 18
|
||||||
|
color: sectionRoot.badgeColor
|
||||||
|
radius: 3
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: badgeText
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: sectionRoot.badge
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: "#FFFFFF"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.leftMargin: 12
|
||||||
|
Layout.topMargin: 8
|
||||||
|
Layout.bottomMargin: 4
|
||||||
|
sourceComponent: sectionRoot.sectionContent
|
||||||
|
visible: sectionRoot.expanded
|
||||||
|
active: sectionRoot.expanded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOpened: {
|
||||||
|
if (root.activeRulesCount > 0) {
|
||||||
|
root.ruleSelected(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
145
ChatView/qml/controls/FileEditsActionBar.qml
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
151
ChatView/qml/controls/FileMentionPopup.qml
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Copyright (C) 2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
|
||||||
|
FileMentionItem {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal selectionRequested()
|
||||||
|
|
||||||
|
visible: searchResults.length > 0
|
||||||
|
height: Math.min(searchResults.length * 36, 36 * 6) + 2
|
||||||
|
|
||||||
|
onCurrentIndexChanged: {
|
||||||
|
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
color: palette.window
|
||||||
|
border.color: palette.mid
|
||||||
|
border.width: 1
|
||||||
|
radius: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listView
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 1
|
||||||
|
model: root.searchResults
|
||||||
|
currentIndex: root.currentIndex
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
ScrollBar.vertical: ScrollBar {
|
||||||
|
policy: ScrollBar.AsNeeded
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
id: delegateItem
|
||||||
|
|
||||||
|
required property int index
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
readonly property bool isProject: modelData.isProject === true
|
||||||
|
readonly property bool isOpen: modelData.isOpen === true
|
||||||
|
readonly property string fileName: {
|
||||||
|
if (isProject)
|
||||||
|
return modelData.projectName
|
||||||
|
const parts = modelData.relativePath.split('/')
|
||||||
|
return parts[parts.length - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
width: listView.width
|
||||||
|
height: 36
|
||||||
|
color: index === root.currentIndex
|
||||||
|
? palette.highlight
|
||||||
|
: (hoverArea.containsMouse
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
|
||||||
|
: "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: 18
|
||||||
|
Layout.preferredHeight: 18
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 3
|
||||||
|
visible: delegateItem.isProject || delegateItem.isOpen
|
||||||
|
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.2)
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return Qt.rgba(palette.highlight.r,
|
||||||
|
palette.highlight.g,
|
||||||
|
palette.highlight.b, 0.3)
|
||||||
|
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: delegateItem.isProject ? "P" : "O"
|
||||||
|
font.bold: true
|
||||||
|
font.pixelSize: 10
|
||||||
|
color: {
|
||||||
|
if (delegateItem.index === root.currentIndex)
|
||||||
|
return palette.highlightedText
|
||||||
|
if (delegateItem.isProject)
|
||||||
|
return palette.highlight
|
||||||
|
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.preferredWidth: 160
|
||||||
|
text: delegateItem.fileName
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? palette.highlightedText
|
||||||
|
: (delegateItem.isProject ? palette.highlight : palette.text)
|
||||||
|
font.bold: true
|
||||||
|
font.italic: delegateItem.isProject
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: delegateItem.isProject
|
||||||
|
? "→"
|
||||||
|
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
|
||||||
|
color: delegateItem.index === root.currentIndex
|
||||||
|
? (delegateItem.isProject
|
||||||
|
? palette.highlightedText
|
||||||
|
: Qt.rgba(palette.highlightedText.r,
|
||||||
|
palette.highlightedText.g,
|
||||||
|
palette.highlightedText.b, 0.7))
|
||||||
|
: palette.mid
|
||||||
|
font.pixelSize: delegateItem.isProject ? 12 : 11
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hoverArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
root.currentIndex = delegateItem.index
|
||||||
|
root.selectionRequested()
|
||||||
|
}
|
||||||
|
onEntered: root.currentIndex = delegateItem.index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
ChatView/qml/controls/SplitDropZone.qml
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal filesDroppedToAttach(var urlStrings)
|
||||||
|
signal filesDroppedToLink(var urlStrings)
|
||||||
|
|
||||||
|
property string activeZone: ""
|
||||||
|
property int filesCount: 0
|
||||||
|
property bool isDragActive: false
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: splitDropOverlay
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: false
|
||||||
|
z: 999
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
topMargin: 30
|
||||||
|
}
|
||||||
|
width: fileCountText.width + 40
|
||||||
|
height: 50
|
||||||
|
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
|
||||||
|
radius: 25
|
||||||
|
visible: root.filesCount > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: fileCountText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("%n file(s) to drop", "", root.filesCount)
|
||||||
|
font.pixelSize: 16
|
||||||
|
font.bold: true
|
||||||
|
color: palette.highlightedText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: leftZone
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: parent.width / 2
|
||||||
|
color: root.activeZone === "left"
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
|
||||||
|
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
|
||||||
|
border.width: root.activeZone === "left" ? 3 : 2
|
||||||
|
border.color: root.activeZone === "left"
|
||||||
|
? palette.highlight
|
||||||
|
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 15
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("Attach")
|
||||||
|
font.pixelSize: 24
|
||||||
|
font.bold: true
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("Images & Text Files")
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(for one-time use)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "left" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: rightZone
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: parent.width / 2
|
||||||
|
color: root.activeZone === "right"
|
||||||
|
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
|
||||||
|
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
|
||||||
|
border.width: root.activeZone === "right" ? 3 : 2
|
||||||
|
border.color: root.activeZone === "right"
|
||||||
|
? palette.highlight
|
||||||
|
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 15
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("LINK")
|
||||||
|
font.pixelSize: 24
|
||||||
|
font.bold: true
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("Text Files")
|
||||||
|
font.pixelSize: 14
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.8
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("(added to context)")
|
||||||
|
font.pixelSize: 12
|
||||||
|
font.italic: true
|
||||||
|
color: root.activeZone === "right" ? palette.highlightedText : palette.text
|
||||||
|
opacity: 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color { ColorAnimation { duration: 150 } }
|
||||||
|
Behavior on border.width { NumberAnimation { duration: 150 } }
|
||||||
|
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors {
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: 2
|
||||||
|
color: palette.mid
|
||||||
|
opacity: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: leftDropArea
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: parent.width / 2
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
onEntered: {
|
||||||
|
root.activeZone = "left"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: rightDropArea
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
right: parent.right
|
||||||
|
top: parent.top
|
||||||
|
bottom: parent.bottom
|
||||||
|
}
|
||||||
|
width: parent.width / 2
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
onEntered: {
|
||||||
|
root.activeZone = "right"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DropArea {
|
||||||
|
id: globalDropArea
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
onEntered: (drag) => {
|
||||||
|
if (drag.hasUrls) {
|
||||||
|
root.isDragActive = true
|
||||||
|
root.filesCount = drag.urls.length
|
||||||
|
splitDropOverlay.visible = true
|
||||||
|
splitDropOverlay.opacity = 1
|
||||||
|
root.activeZone = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
root.isDragActive = false
|
||||||
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
if (!root.isDragActive) {
|
||||||
|
splitDropOverlay.visible = false
|
||||||
|
root.activeZone = ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: (drag) => {
|
||||||
|
if (drag.hasUrls) {
|
||||||
|
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onDropped: (drop) => {
|
||||||
|
const targetZone = root.activeZone
|
||||||
|
root.isDragActive = false
|
||||||
|
root.filesCount = 0
|
||||||
|
splitDropOverlay.opacity = 0
|
||||||
|
|
||||||
|
Qt.callLater(function() {
|
||||||
|
splitDropOverlay.visible = false
|
||||||
|
root.activeZone = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!drop.hasUrls || drop.urls.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var urlStrings = []
|
||||||
|
for (var i = 0; i < drop.urls.length; i++) {
|
||||||
|
var urlString = drop.urls[i].toString()
|
||||||
|
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
|
||||||
|
urlStrings.push(urlString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlStrings.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
drop.accept(Qt.CopyAction)
|
||||||
|
|
||||||
|
if (targetZone === "right") {
|
||||||
|
root.filesDroppedToLink(urlStrings)
|
||||||
|
} else {
|
||||||
|
root.filesDroppedToAttach(urlStrings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
87
ChatView/qml/controls/Toast.qml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
280
ChatView/qml/controls/TopBar.qml
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import ChatView
|
||||||
|
import UIControls
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias saveButton: saveButtonId
|
||||||
|
property alias loadButton: loadButtonId
|
||||||
|
property alias clearButton: clearButtonId
|
||||||
|
property alias tokensBadge: tokensBadgeId
|
||||||
|
property alias recentPath: recentPathId
|
||||||
|
property alias openChatHistory: openChatHistoryId
|
||||||
|
property alias pinButton: pinButtonId
|
||||||
|
property alias contextButton: contextButtonId
|
||||||
|
property alias toolsButton: toolsButtonId
|
||||||
|
property alias thinkingMode: thinkingModeId
|
||||||
|
property alias settingsButton: settingsButtonId
|
||||||
|
property alias configSelector: configSelectorId
|
||||||
|
property alias roleSelector: roleSelector
|
||||||
|
|
||||||
|
color: palette.window.hslLightness > 0.5 ?
|
||||||
|
Qt.darker(palette.window, 1.1) :
|
||||||
|
Qt.lighter(palette.window, 1.1)
|
||||||
|
|
||||||
|
Flow {
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
margins: 5
|
||||||
|
}
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: firstRow
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAComboBox {
|
||||||
|
id: configSelectorId
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch saved AI configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAComboBox {
|
||||||
|
id: roleSelector
|
||||||
|
|
||||||
|
implicitHeight: 25
|
||||||
|
|
||||||
|
model: []
|
||||||
|
currentIndex: 0
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Switch agent role (different system prompts)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: toolsButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
checkable: true
|
||||||
|
opacity: enabled ? 1.0 : 0.2
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
|
||||||
|
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: {
|
||||||
|
if (!toolsButtonId.enabled) {
|
||||||
|
return qsTr("Tools are disabled in General Settings")
|
||||||
|
}
|
||||||
|
return checked
|
||||||
|
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
|
||||||
|
: qsTr("Tools disabled: 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: settingsButtonId
|
||||||
|
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Open Chat Assistant Settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
height: firstRow.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 {
|
||||||
|
id: secondRow
|
||||||
|
|
||||||
|
Layout.preferredWidth: root.width
|
||||||
|
Layout.preferredHeight: firstRow.height
|
||||||
|
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: saveButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Save chat to *.json file")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: loadButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Load chat from *.json file")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: openChatHistoryId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Show in system")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: contextButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
|
||||||
|
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
|
||||||
|
height: 15
|
||||||
|
width: 15
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
|
||||||
|
}
|
||||||
|
|
||||||
|
Badge {
|
||||||
|
id: tokensBadgeId
|
||||||
|
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
|
||||||
|
}
|
||||||
|
|
||||||
|
QoASeparator {}
|
||||||
|
|
||||||
|
QoAButton {
|
||||||
|
id: clearButtonId
|
||||||
|
|
||||||
|
icon {
|
||||||
|
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
|
||||||
|
height: 15
|
||||||
|
width: 8
|
||||||
|
}
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: 250
|
||||||
|
ToolTip.text: qsTr("Clean chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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 ChatView
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string code: ""
|
|
||||||
property string language: ""
|
|
||||||
|
|
||||||
readonly property string monospaceFont: {
|
|
||||||
switch (Qt.platform.os) {
|
|
||||||
case "windows":
|
|
||||||
return "Consolas";
|
|
||||||
case "osx":
|
|
||||||
return "Menlo";
|
|
||||||
case "linux":
|
|
||||||
return "DejaVu Sans Mono";
|
|
||||||
default:
|
|
||||||
return "monospace";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
color: palette.alternateBase
|
|
||||||
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
|
|
||||||
: Qt.lighter(root.color, 1.3)
|
|
||||||
border.width: 2
|
|
||||||
radius: 4
|
|
||||||
|
|
||||||
implicitWidth: parent.width
|
|
||||||
implicitHeight: codeText.implicitHeight + 20
|
|
||||||
|
|
||||||
ChatUtils {
|
|
||||||
id: utils
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: codeText
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 10
|
|
||||||
text: root.code
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
font.family: root.monospaceFont
|
|
||||||
font.pointSize: Qt.application.font.pointSize
|
|
||||||
color: parent.color.hslLightness > 0.5 ? "black" : "white"
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
}
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 5
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
text: root.language
|
|
||||||
color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.1)
|
|
||||||
: Qt.lighter(root.color, 1.1)
|
|
||||||
font.pointSize: 8
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: 5
|
|
||||||
text: "Copy"
|
|
||||||
onClicked: {
|
|
||||||
utils.copyToClipboard(root.code)
|
|
||||||
text = qsTr("Copied")
|
|
||||||
copyTimer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
id: copyTimer
|
|
||||||
interval: 2000
|
|
||||||
onTriggered: parent.text = qsTr("Copy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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
|
|
||||||
|
|
||||||
TextEdit {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
readOnly: true
|
|
||||||
selectByMouse: true
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
textFormat: Text.StyledText
|
|
||||||
selectionColor: palette.highlight
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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 ChatView
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property alias sendButton: sendButtonId
|
|
||||||
property alias stopButton: stopButtonId
|
|
||||||
property alias syncOpenFiles: syncOpenFilesId
|
|
||||||
property alias attachFiles: attachFilesId
|
|
||||||
property alias linkFiles: linkFilesId
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
|
||||||
Qt.darker(palette.window, 1.1) :
|
|
||||||
Qt.lighter(palette.window, 1.1)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: bottomBar
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 5
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 5
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: sendButtonId
|
|
||||||
|
|
||||||
text: qsTr("Send")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: stopButtonId
|
|
||||||
|
|
||||||
text: qsTr("Stop")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: attachFilesId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
|
|
||||||
height: 15
|
|
||||||
width: 8
|
|
||||||
}
|
|
||||||
text: qsTr("Attach files")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: linkFilesId
|
|
||||||
|
|
||||||
icon {
|
|
||||||
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
|
|
||||||
height: 15
|
|
||||||
width: 8
|
|
||||||
}
|
|
||||||
text: qsTr("Link files")
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox {
|
|
||||||
id: syncOpenFilesId
|
|
||||||
|
|
||||||
text: qsTr("Sync open files")
|
|
||||||
|
|
||||||
ToolTip.visible: syncOpenFilesId.hovered
|
|
||||||
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2024 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.Layouts
|
|
||||||
import ChatView
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property alias saveButton: saveButtonId
|
|
||||||
property alias loadButton: loadButtonId
|
|
||||||
property alias clearButton: clearButtonId
|
|
||||||
property alias tokensBadge: tokensBadgeId
|
|
||||||
property alias recentPath: recentPathId
|
|
||||||
property alias openChatHistory: openChatHistoryId
|
|
||||||
|
|
||||||
color: palette.window.hslLightness > 0.5 ?
|
|
||||||
Qt.darker(palette.window, 1.1) :
|
|
||||||
Qt.lighter(palette.window, 1.1)
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
anchors {
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 5
|
|
||||||
right: parent.right
|
|
||||||
rightMargin: 5
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
spacing: 10
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: saveButtonId
|
|
||||||
|
|
||||||
text: qsTr("Save")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: loadButtonId
|
|
||||||
|
|
||||||
text: qsTr("Load")
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: clearButtonId
|
|
||||||
|
|
||||||
text: qsTr("Clear")
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
id: recentPathId
|
|
||||||
|
|
||||||
elide: Text.ElideMiddle
|
|
||||||
color: palette.text
|
|
||||||
}
|
|
||||||
|
|
||||||
QoAButton {
|
|
||||||
id: openChatHistoryId
|
|
||||||
|
|
||||||
text: qsTr("Show in system")
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Badge {
|
|
||||||
id: tokensBadgeId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +1,9 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
||||||
* Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|
||||||
@@ -32,6 +17,44 @@ struct LanguageProperties
|
|||||||
QVector<QString> fileExtensions;
|
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()
|
const QVector<LanguageProperties> &getKnownLanguages()
|
||||||
{
|
{
|
||||||
static QVector<LanguageProperties> knownLanguages = {
|
static QVector<LanguageProperties> knownLanguages = {
|
||||||
@@ -52,11 +75,27 @@ const QVector<LanguageProperties> &getKnownLanguages()
|
|||||||
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
|
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
|
||||||
{"perl", "#", {"pl", "perl"}, {"pl"}},
|
{"perl", "#", {"pl", "perl"}, {"pl"}},
|
||||||
{"hs", "--", {"hs", "haskell"}, {"hs"}},
|
{"hs", "--", {"hs", "haskell"}, {"hs"}},
|
||||||
|
{"qml", "//", {"qml"}, {"qml"}},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
knownLanguages.append(customLanguagesFromSettings());
|
||||||
|
|
||||||
return knownLanguages;
|
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()
|
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
|
||||||
{
|
{
|
||||||
QHash<QString, QString> result;
|
QHash<QString, QString> result;
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
@@ -40,6 +24,11 @@ public:
|
|||||||
*/
|
*/
|
||||||
static QString detectLanguageFromExtension(const QString &extension);
|
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);
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "ConfigurationManager.hpp"
|
#include "ConfigurationManager.hpp"
|
||||||
|
|
||||||
@@ -36,11 +20,12 @@ void ConfigurationManager::init()
|
|||||||
{
|
{
|
||||||
setupConnections();
|
setupConnections();
|
||||||
updateAllTemplateDescriptions();
|
updateAllTemplateDescriptions();
|
||||||
|
checkAllTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
|
||||||
{
|
{
|
||||||
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
|
||||||
|
|
||||||
if (!templ) {
|
if (!templ) {
|
||||||
return;
|
return;
|
||||||
@@ -50,6 +35,8 @@ void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &
|
|||||||
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
m_generalSettings.ccTemplateDescription.setValue(templ->description());
|
||||||
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
} else if (&templateAspect == &m_generalSettings.caTemplate) {
|
||||||
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
m_generalSettings.caTemplateDescription.setValue(templ->description());
|
||||||
|
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
|
||||||
|
m_generalSettings.qrTemplateDescription.setValue(templ->description());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,13 +44,34 @@ void ConfigurationManager::updateAllTemplateDescriptions()
|
|||||||
{
|
{
|
||||||
updateTemplateDescription(m_generalSettings.ccTemplate);
|
updateTemplateDescription(m_generalSettings.ccTemplate);
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
updateTemplateDescription(m_generalSettings.caTemplate);
|
||||||
|
updateTemplateDescription(m_generalSettings.qrTemplate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
|
||||||
|
{
|
||||||
|
PluginLLMCore::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)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
, m_generalSettings(Settings::generalSettings())
|
, m_generalSettings(Settings::generalSettings())
|
||||||
, m_providersManager(LLMCore::ProvidersManager::instance())
|
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
|
||||||
, m_templateManger(LLMCore::PromptTemplateManager::instance())
|
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void ConfigurationManager::setupConnections()
|
void ConfigurationManager::setupConnections()
|
||||||
@@ -73,12 +81,16 @@ 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);
|
||||||
@@ -94,6 +106,10 @@ void ConfigurationManager::setupConnections()
|
|||||||
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
|
||||||
updateTemplateDescription(m_generalSettings.caTemplate);
|
updateTemplateDescription(m_generalSettings.caTemplate);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
|
||||||
|
updateTemplateDescription(m_generalSettings.qrTemplate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigurationManager::selectProvider()
|
void ConfigurationManager::selectProvider()
|
||||||
@@ -108,6 +124,8 @@ 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] {
|
||||||
@@ -124,35 +142,37 @@ 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
|
||||||
: m_generalSettings.caModel;
|
: isQuickRefactor ? m_generalSettings.qrModel
|
||||||
|
: m_generalSettings.caModel);
|
||||||
|
|
||||||
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
if (auto provider = m_providersManager.getProviderByName(providerName)) {
|
||||||
if (!provider->supportsModelListing()) {
|
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
|
||||||
m_generalSettings.showModelsNotSupportedDialog(targetSettings);
|
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto modelList = provider->getInstalledModels(providerUrl);
|
provider->getInstalledModels(providerUrl)
|
||||||
|
.then(this, [this, targetSettings](const QList<QString> &modelList) {
|
||||||
if (modelList.isEmpty()) {
|
if (modelList.isEmpty()) {
|
||||||
m_generalSettings.showModelsNotFoundDialog(targetSettings);
|
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QTimer::singleShot(0, &m_generalSettings, [this, modelList, &targetSettings]() {
|
|
||||||
m_generalSettings.showSelectionDialog(
|
m_generalSettings.showSelectionDialog(
|
||||||
modelList, targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,8 +185,10 @@ 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()
|
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();
|
||||||
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
|
||||||
|
|
||||||
@@ -176,6 +198,7 @@ void ConfigurationManager::selectTemplate()
|
|||||||
|
|
||||||
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]() {
|
||||||
@@ -200,6 +223,8 @@ 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]() {
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
/*
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include "llmcore/PromptTemplateManager.hpp"
|
#include "pluginllmcore/PromptTemplateManager.hpp"
|
||||||
#include "llmcore/ProvidersManager.hpp"
|
#include "pluginllmcore/ProvidersManager.hpp"
|
||||||
#include "settings/GeneralSettings.hpp"
|
#include "settings/GeneralSettings.hpp"
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
@@ -38,6 +22,8 @@ public:
|
|||||||
|
|
||||||
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
|
||||||
void updateAllTemplateDescriptions();
|
void updateAllTemplateDescriptions();
|
||||||
|
void checkTemplate(const Utils::StringAspect &templateAspect);
|
||||||
|
void checkAllTemplate();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void selectProvider();
|
void selectProvider();
|
||||||
@@ -52,8 +38,8 @@ private:
|
|||||||
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
|
||||||
|
|
||||||
Settings::GeneralSettings &m_generalSettings;
|
Settings::GeneralSettings &m_generalSettings;
|
||||||
LLMCore::ProvidersManager &m_providersManager;
|
PluginLLMCore::ProvidersManager &m_providersManager;
|
||||||
LLMCore::PromptTemplateManager &m_templateManger;
|
PluginLLMCore::PromptTemplateManager &m_templateManger;
|
||||||
|
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,60 +1,48 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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 "LLMClientInterface.hpp"
|
#include "LLMClientInterface.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
|
|
||||||
#include <llmcore/RequestConfig.hpp>
|
|
||||||
#include <texteditor/textdocument.h>
|
|
||||||
|
|
||||||
#include "CodeHandler.hpp"
|
#include "CodeHandler.hpp"
|
||||||
#include "context/ContextManager.hpp"
|
|
||||||
#include "context/DocumentContextReader.hpp"
|
#include "context/DocumentContextReader.hpp"
|
||||||
#include "context/Utils.hpp"
|
#include "context/Utils.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 <pluginllmcore/RulesLoader.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
LLMClientInterface::LLMClientInterface(
|
LLMClientInterface::LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings)
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
: m_requestHandler(this)
|
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||||
, m_generalSettings(generalSettings)
|
PluginLLMCore::IPromptProvider *promptProvider,
|
||||||
|
Context::IDocumentReader &documentReader,
|
||||||
|
IRequestPerformanceLogger &performanceLogger)
|
||||||
|
: m_generalSettings(generalSettings)
|
||||||
, m_completeSettings(completeSettings)
|
, 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 "Qode Assist";
|
return "QodeAssist";
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::startImpl()
|
void LLMClientInterface::startImpl()
|
||||||
@@ -62,6 +50,45 @@ 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;
|
||||||
|
|
||||||
|
const RequestContext &ctx = it.value();
|
||||||
|
|
||||||
|
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
|
||||||
|
|
||||||
|
// Send LSP error response to client
|
||||||
|
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);
|
||||||
@@ -80,11 +107,9 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
} else if (method == "textDocument/didOpen") {
|
} else if (method == "textDocument/didOpen") {
|
||||||
handleTextDocumentDidOpen(request);
|
handleTextDocumentDidOpen(request);
|
||||||
} else if (method == "getCompletionsCycling") {
|
} else if (method == "getCompletionsCycling") {
|
||||||
QString requestId = request["id"].toString();
|
|
||||||
startTimeMeasurement(requestId);
|
|
||||||
handleCompletion(request);
|
handleCompletion(request);
|
||||||
} else if (method == "$/cancelRequest") {
|
} else if (method == "$/cancelRequest") {
|
||||||
handleCancelRequest(request);
|
handleCancelRequest();
|
||||||
} else if (method == "exit") {
|
} else if (method == "exit") {
|
||||||
// TODO make exit handler
|
// TODO make exit handler
|
||||||
} else {
|
} else {
|
||||||
@@ -92,14 +117,29 @@ void LLMClientInterface::sendData(const QByteArray &data)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
|
void LLMClientInterface::handleCancelRequest()
|
||||||
{
|
{
|
||||||
QString id = request["params"].toObject()["id"].toString();
|
QSet<PluginLLMCore::Provider *> providers;
|
||||||
if (m_requestHandler.cancelRequest(id)) {
|
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
|
||||||
LOG_MESSAGE(QString("Request %1 cancelled successfully").arg(id));
|
if (it.value().provider) {
|
||||||
} else {
|
providers.insert(it.value().provider);
|
||||||
LOG_MESSAGE(QString("Request %1 not found").arg(id));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto *provider : providers) {
|
||||||
|
disconnect(provider->client(), 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)
|
||||||
@@ -152,11 +192,38 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
|
|||||||
emit finished();
|
emit finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
|
||||||
|
{
|
||||||
|
QJsonObject response;
|
||||||
|
response["jsonrpc"] = "2.0";
|
||||||
|
response[LanguageServerProtocol::idKey] = request["id"];
|
||||||
|
|
||||||
|
QJsonObject errorObject;
|
||||||
|
errorObject["code"] = -32603; // Internal error code
|
||||||
|
errorObject["message"] = errorMessage;
|
||||||
|
response["error"] = errorObject;
|
||||||
|
|
||||||
|
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
|
||||||
|
|
||||||
|
// 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 updatedContext = prepareContext(request);
|
auto filePath = Context::extractFilePathFromRequest(request);
|
||||||
|
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||||
|
if (!documentInfo.document) {
|
||||||
|
QString error = QString("Document is not available: %1").arg(filePath);
|
||||||
|
LOG_MESSAGE("Error: " + error);
|
||||||
|
sendErrorResponse(request, error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool isPreset1Active = Context::ContextManager::isSpecifyCompletion(request, m_generalSettings);
|
auto updatedContext = prepareContext(request, documentInfo);
|
||||||
|
|
||||||
|
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||||
|
|
||||||
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
const auto providerName = !isPreset1Active ? m_generalSettings.ccProvider()
|
||||||
: m_generalSettings.ccPreset1Provider();
|
: m_generalSettings.ccPreset1Provider();
|
||||||
@@ -165,61 +232,71 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
const auto url = !isPreset1Active ? m_generalSettings.ccUrl()
|
||||||
: m_generalSettings.ccPreset1Url();
|
: m_generalSettings.ccPreset1Url();
|
||||||
|
|
||||||
const auto provider = LLMCore::ProvidersManager::instance().getProviderByName(providerName);
|
const auto provider = m_providerRegistry.getProviderByName(providerName);
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
|
QString error = 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 ? m_generalSettings.ccTemplate()
|
||||||
: m_generalSettings.ccPreset1Template();
|
: m_generalSettings.ccPreset1Template();
|
||||||
|
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||||
templateName);
|
|
||||||
|
|
||||||
if (!promptTemplate) {
|
if (!promptTemplate) {
|
||||||
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
|
QString error = QString("No template found with name: %1").arg(templateName);
|
||||||
|
LOG_MESSAGE(error);
|
||||||
|
sendErrorResponse(request, error);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor to dynamic presets system
|
QJsonObject payload{{"model", modelName}, {"stream", true}};
|
||||||
LLMCore::LLMConfig config;
|
|
||||||
config.requestType = LLMCore::RequestType::CodeCompletion;
|
|
||||||
config.provider = provider;
|
|
||||||
config.promptTemplate = promptTemplate;
|
|
||||||
// TODO refactor networking
|
|
||||||
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
|
|
||||||
QString stream = m_completeSettings.stream() ? QString{"streamGenerateContent?alt=sse"}
|
|
||||||
: QString{"generateContent?"};
|
|
||||||
config.url = QUrl(QString("%1/models/%2:%3").arg(url, modelName, stream));
|
|
||||||
} else {
|
|
||||||
config.url = QUrl(QString("%1%2").arg(
|
|
||||||
url,
|
|
||||||
promptTemplate->type() == LLMCore::TemplateType::FIM ? provider->completionEndpoint()
|
|
||||||
: provider->chatEndpoint()));
|
|
||||||
config.providerRequest = {{"model", modelName}, {"stream", m_completeSettings.stream()}};
|
|
||||||
}
|
|
||||||
config.apiKey = provider->apiKey();
|
|
||||||
config.multiLineCompletion = m_completeSettings.multiLineCompletion();
|
|
||||||
|
|
||||||
const auto stopWords = QJsonArray::fromStringList(config.promptTemplate->stopWords());
|
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
|
||||||
if (!stopWords.isEmpty())
|
if (!stopWords.isEmpty())
|
||||||
config.providerRequest["stop"] = stopWords;
|
payload["stop"] = stopWords;
|
||||||
|
|
||||||
QString systemPrompt;
|
QString systemPrompt;
|
||||||
if (m_completeSettings.useSystemPrompt())
|
if (m_completeSettings.useSystemPrompt())
|
||||||
systemPrompt.append(
|
systemPrompt.append(
|
||||||
m_completeSettings.useUserMessageTemplateForCC()
|
m_completeSettings.useUserMessageTemplateForCC()
|
||||||
&& promptTemplate->type() == LLMCore::TemplateType::Chat
|
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
|
||||||
? m_completeSettings.systemPromptForNonFimModels()
|
? m_completeSettings.systemPromptForNonFimModels()
|
||||||
: m_completeSettings.systemPrompt());
|
: m_completeSettings.systemPrompt());
|
||||||
|
|
||||||
|
auto project = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
|
if (project) {
|
||||||
|
QString projectRules
|
||||||
|
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::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())
|
if (updatedContext.fileContext.has_value())
|
||||||
systemPrompt.append(updatedContext.fileContext.value());
|
systemPrompt.append(updatedContext.fileContext.value());
|
||||||
|
|
||||||
|
if (m_completeSettings.useOpenFilesContext()) {
|
||||||
|
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
|
||||||
|
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
|
||||||
|
if (!updatedContext.filesMetadata) {
|
||||||
|
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
|
||||||
|
}
|
||||||
|
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updatedContext.systemPrompt = systemPrompt;
|
updatedContext.systemPrompt = systemPrompt;
|
||||||
|
|
||||||
if (promptTemplate->type() == LLMCore::TemplateType::Chat) {
|
if (promptTemplate->type() == PluginLLMCore::TemplateType::Chat) {
|
||||||
QString userMessage;
|
QString userMessage;
|
||||||
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
if (m_completeSettings.useUserMessageTemplateForCC()) {
|
||||||
userMessage = m_completeSettings.processMessageToFIM(
|
userMessage = m_completeSettings.processMessageToFIM(
|
||||||
@@ -229,60 +306,76 @@ void LLMClientInterface::handleCompletion(const QJsonObject &request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO refactor add message
|
// TODO refactor add message
|
||||||
QVector<LLMCore::Message> messages;
|
QVector<PluginLLMCore::Message> messages;
|
||||||
messages.append({"user", userMessage});
|
messages.append({"user", userMessage});
|
||||||
updatedContext.history = messages;
|
updatedContext.history = messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.provider->prepareRequest(
|
provider->prepareRequest(
|
||||||
config.providerRequest,
|
payload,
|
||||||
promptTemplate,
|
promptTemplate,
|
||||||
updatedContext,
|
updatedContext,
|
||||||
LLMCore::RequestType::CodeCompletion);
|
PluginLLMCore::RequestType::CodeCompletion,
|
||||||
|
false,
|
||||||
|
false);
|
||||||
|
|
||||||
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
|
connect(
|
||||||
if (!errors.isEmpty()) {
|
provider->client(),
|
||||||
LOG_MESSAGE("Validate errors for fim request:");
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
LOG_MESSAGES(errors);
|
this,
|
||||||
return;
|
&LLMClientInterface::handleFullResponse,
|
||||||
}
|
Qt::UniqueConnection);
|
||||||
m_requestHandler.sendLLMRequest(config, request);
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&LLMClientInterface::handleRequestFailed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
|
||||||
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
m_performanceLogger.startTimeMeasurement(requestId);
|
||||||
}
|
}
|
||||||
|
|
||||||
LLMCore::ContextData LLMClientInterface::prepareContext(
|
PluginLLMCore::ContextData LLMClientInterface::prepareContext(
|
||||||
const QJsonObject &request, const QStringView &accumulatedCompletion)
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo)
|
||||||
{
|
{
|
||||||
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();
|
||||||
|
|
||||||
auto filePath = Context::extractFilePathFromRequest(request);
|
|
||||||
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
|
|
||||||
Utils::FilePath::fromString(filePath));
|
|
||||||
|
|
||||||
if (!textDocument) {
|
|
||||||
LOG_MESSAGE("Error: Document is not available for" + filePath);
|
|
||||||
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->document(), textDocument->mimeType(), filePath);
|
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
|
||||||
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString LLMClientInterface::resolveEndpoint(
|
||||||
|
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
|
||||||
|
{
|
||||||
|
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
|
||||||
|
: m_generalSettings.ccCustomEndpoint();
|
||||||
|
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
Context::ContextManager *LLMClientInterface::contextManager() const
|
||||||
|
{
|
||||||
|
return m_contextManager;
|
||||||
|
}
|
||||||
|
|
||||||
void LLMClientInterface::sendCompletionToClient(
|
void LLMClientInterface::sendCompletionToClient(
|
||||||
const QString &completion, const QJsonObject &request, bool isComplete)
|
const QString &completion, const QJsonObject &request, bool isComplete)
|
||||||
{
|
{
|
||||||
bool isPreset1Active = Context::ContextManager::isSpecifyCompletion(request, m_generalSettings);
|
auto filePath = Context::extractFilePathFromRequest(request);
|
||||||
|
auto documentInfo = m_documentReader.readDocument(filePath);
|
||||||
|
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
|
||||||
|
|
||||||
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
|
||||||
: m_generalSettings.ccPreset1Template();
|
: m_generalSettings.ccPreset1Template();
|
||||||
|
|
||||||
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getFimTemplateByName(
|
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
|
||||||
templateName);
|
|
||||||
|
|
||||||
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
|
||||||
|
|
||||||
@@ -296,16 +389,36 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
|
|
||||||
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
|
LOG_MESSAGE(QString("Completions before filter: \n%1").arg(completion));
|
||||||
|
|
||||||
QString processedCompletion
|
QString outputHandler = m_completeSettings.modelOutputHandler.stringValue();
|
||||||
= promptTemplate->type() == LLMCore::TemplateType::Chat
|
QString processedCompletion;
|
||||||
&& m_completeSettings.smartProcessInstuctText()
|
|
||||||
? CodeHandler::processText(completion, Context::extractFilePathFromRequest(request))
|
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;
|
range["end"] = position;
|
||||||
|
|
||||||
completionItem[LanguageServerProtocol::rangeKey] = range;
|
completionItem[LanguageServerProtocol::rangeKey] = range;
|
||||||
completionItem[LanguageServerProtocol::positionKey] = position;
|
completionItem[LanguageServerProtocol::positionKey] = position;
|
||||||
completions.append(completionItem);
|
completions.append(completionItem);
|
||||||
@@ -322,32 +435,8 @@ void LLMClientInterface::sendCompletionToClient(
|
|||||||
.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();
|
||||||
endTimeMeasurement(requestId);
|
m_performanceLogger.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
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#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 <pluginllmcore/ContextData.hpp>
|
||||||
#include <llmcore/RequestHandler.hpp>
|
#include <pluginllmcore/IPromptProvider.hpp>
|
||||||
|
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||||
|
#include <logger/IRequestPerformanceLogger.hpp>
|
||||||
#include <settings/CodeCompletionSettings.hpp>
|
#include <settings/CodeCompletionSettings.hpp>
|
||||||
#include <settings/GeneralSettings.hpp>
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
|
||||||
@@ -40,7 +28,12 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
|
|||||||
public:
|
public:
|
||||||
LLMClientInterface(
|
LLMClientInterface(
|
||||||
const Settings::GeneralSettings &generalSettings,
|
const Settings::GeneralSettings &generalSettings,
|
||||||
const Settings::CodeCompletionSettings &completeSettings);
|
const Settings::CodeCompletionSettings &completeSettings,
|
||||||
|
PluginLLMCore::IProviderRegistry &providerRegistry,
|
||||||
|
PluginLLMCore::IPromptProvider *promptProvider,
|
||||||
|
Context::IDocumentReader &documentReader,
|
||||||
|
IRequestPerformanceLogger &performanceLogger);
|
||||||
|
~LLMClientInterface() override;
|
||||||
|
|
||||||
Utils::FilePath serverDeviceTemplate() const override;
|
Utils::FilePath serverDeviceTemplate() const override;
|
||||||
|
|
||||||
@@ -49,10 +42,17 @@ public:
|
|||||||
|
|
||||||
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;
|
|
||||||
void parseCurrentMessage() override;
|
private slots:
|
||||||
|
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);
|
||||||
@@ -60,20 +60,30 @@ 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(const QJsonObject &request);
|
void handleCancelRequest();
|
||||||
|
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
|
||||||
|
|
||||||
LLMCore::ContextData prepareContext(
|
struct RequestContext
|
||||||
const QJsonObject &request, const QStringView &accumulatedCompletion = QString{});
|
{
|
||||||
|
QJsonObject originalRequest;
|
||||||
|
PluginLLMCore::Provider *provider;
|
||||||
|
};
|
||||||
|
|
||||||
|
PluginLLMCore::ContextData prepareContext(
|
||||||
|
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
|
||||||
|
|
||||||
|
QString resolveEndpoint(
|
||||||
|
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
|
||||||
|
|
||||||
const Settings::CodeCompletionSettings &m_completeSettings;
|
const Settings::CodeCompletionSettings &m_completeSettings;
|
||||||
const Settings::GeneralSettings &m_generalSettings;
|
const Settings::GeneralSettings &m_generalSettings;
|
||||||
LLMCore::RequestHandler m_requestHandler;
|
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
|
||||||
|
PluginLLMCore::IProviderRegistry &m_providerRegistry;
|
||||||
|
Context::IDocumentReader &m_documentReader;
|
||||||
|
IRequestPerformanceLogger &m_performanceLogger;
|
||||||
QElapsedTimer m_completionTimer;
|
QElapsedTimer m_completionTimer;
|
||||||
QMap<QString, qint64> m_requestStartTimes;
|
Context::ContextManager *m_contextManager;
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,26 +1,6 @@
|
|||||||
/*
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of QodeAssist.
|
|
||||||
*
|
|
||||||
* The Qt Company portions:
|
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
||||||
*
|
|
||||||
* Petr Mironychev portions:
|
|
||||||
* 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 "LLMSuggestion.hpp"
|
#include "LLMSuggestion.hpp"
|
||||||
#include <texteditor/texteditor.h>
|
#include <texteditor/texteditor.h>
|
||||||
@@ -29,6 +9,46 @@
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
static bool isClosingTail(const QString &s, int from)
|
||||||
|
{
|
||||||
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
|
for (int i = from; i < s.size(); ++i) {
|
||||||
|
const QChar c = s.at(i);
|
||||||
|
if (!c.isSpace() && !closeChars.contains(c))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
|
||||||
|
{
|
||||||
|
if (rightText.isEmpty())
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const int maxN = qMin(suggestion.size(), rightText.size());
|
||||||
|
int lcp = 0;
|
||||||
|
while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp))
|
||||||
|
++lcp;
|
||||||
|
|
||||||
|
if (lcp > 0) {
|
||||||
|
if (isClosingTail(rightText, lcp))
|
||||||
|
return rightText.size();
|
||||||
|
return lcp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClosingTail(rightText, 0))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
static const QString closeChars = QStringLiteral("(){}[];,");
|
||||||
|
int i = suggestion.size() - 1;
|
||||||
|
while (i >= 0 && suggestion.at(i).isSpace())
|
||||||
|
--i;
|
||||||
|
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
|
||||||
|
return rightText.size();
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -36,23 +56,37 @@ 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() - 1);
|
startPos = qBound(0, startPos, sourceDocument->characterCount());
|
||||||
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 startPosInBlock = startPos - block.position();
|
int cursorPositionInBlock = cursor.positionInBlock();
|
||||||
int endPosInBlock = endPos - block.position();
|
QString leftText = blockText.left(cursorPositionInBlock);
|
||||||
|
QString rightText = blockText.mid(cursorPositionInBlock);
|
||||||
|
|
||||||
blockText.replace(startPosInBlock, endPosInBlock - startPosInBlock, data.text);
|
QString suggestionText = data.text;
|
||||||
replacementDocument()->setPlainText(blockText);
|
|
||||||
|
if (!suggestionText.contains('\n')) {
|
||||||
|
int replaceLength = calculateReplaceLength(suggestionText, rightText);
|
||||||
|
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);
|
||||||
|
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)
|
||||||
@@ -67,41 +101,122 @@ bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
|
|||||||
|
|
||||||
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
|
||||||
{
|
{
|
||||||
const Utils::Text::Range range = suggestions()[currentSuggestion()].range;
|
const auto ¤tSuggestions = suggestions();
|
||||||
|
const auto ¤tData = 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 = suggestions()[currentSuggestion()].text;
|
const QString text = currentData.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())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
|
if (subText.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startPos == 0) {
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
|
|
||||||
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
|
|
||||||
|
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
|
const Utils::Text::Position newEnd{newStart.line, int(newCompletionText.length())};
|
||||||
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 ¤tSuggestions = suggestions();
|
||||||
|
const auto ¤tData = 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 textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (replaceLength > 0) {
|
||||||
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.insertText(firstLine + restOfText);
|
||||||
|
} else {
|
||||||
|
int replaceLength = calculateReplaceLength(text, textAfterCursor);
|
||||||
|
|
||||||
|
if (replaceLength > 0) {
|
||||||
|
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.endEditBlock();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
@@ -40,5 +40,8 @@ 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);
|
||||||
};
|
};
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of QodeAssist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
{
|
{
|
||||||
"Id" : "qodeassist",
|
"Id" : "qodeassist",
|
||||||
"Name" : "QodeAssist",
|
"Name" : "QodeAssist",
|
||||||
"Version" : "0.5.1",
|
"Version" : "0.9.12",
|
||||||
|
"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" : "",
|
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
|
||||||
${IDE_PLUGIN_DEPENDENCIES}
|
${IDE_PLUGIN_DEPENDENCIES}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,5 +2,13 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
* Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
* Copyright (C) 2024-2026 Petr Mironychev
|
||||||
*
|
*
|
||||||
* This file is part of Qode Assist.
|
* This file is part of QodeAssist.
|
||||||
*
|
*
|
||||||
* 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,17 +24,27 @@
|
|||||||
|
|
||||||
#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 "settings/QuickRefactorSettings.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
|
#include "RefactorContextHelper.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;
|
||||||
@@ -44,12 +54,96 @@ using namespace Core;
|
|||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
QodeAssistClient::QodeAssistClient()
|
namespace {
|
||||||
: LanguageClient::Client(
|
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
|
||||||
new LLMClientInterface(Settings::generalSettings(), Settings::codeCompletionSettings()))
|
{
|
||||||
|
return Utils::Text::Position{pos.line, pos.column};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isIdentifierChar(QChar c)
|
||||||
|
{
|
||||||
|
return c.isLetterOrNumber() || c == QLatin1Char('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isInsideIdentifier(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0 || col > text.size())
|
||||||
|
return false;
|
||||||
|
if (!isIdentifierChar(text.at(col - 1)))
|
||||||
|
return false;
|
||||||
|
return col < text.size() && isIdentifierChar(text.at(col));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterMemberAccess(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
if (col <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && isIdentifierChar(text.at(i)))
|
||||||
|
--i;
|
||||||
|
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
if (c == QLatin1Char('.'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
|
||||||
|
return true;
|
||||||
|
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isFreshIndentedLine(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
if (col == 0)
|
||||||
|
return false;
|
||||||
|
const QString leftText = block.text().left(col);
|
||||||
|
for (const QChar &ch : leftText) {
|
||||||
|
if (!ch.isSpace())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isAfterEagerTrigger(const QTextCursor &cursor)
|
||||||
|
{
|
||||||
|
const QTextBlock block = cursor.block();
|
||||||
|
const int col = cursor.positionInBlock();
|
||||||
|
const QString text = block.text();
|
||||||
|
int i = col - 1;
|
||||||
|
while (i >= 0 && text.at(i).isSpace())
|
||||||
|
--i;
|
||||||
|
if (i < 0)
|
||||||
|
return false;
|
||||||
|
const QChar c = text.at(i);
|
||||||
|
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|
||||||
|
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|
||||||
|
|| c == QLatin1Char(':') || c == QLatin1Char('>');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isManualMode()
|
||||||
|
{
|
||||||
|
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
|
||||||
|
}
|
||||||
|
} // anonymous namespace
|
||||||
|
|
||||||
|
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
|
||||||
|
: LanguageClient::Client(clientInterface)
|
||||||
|
, m_llmClient(clientInterface)
|
||||||
, m_recentCharCount(0)
|
, m_recentCharCount(0)
|
||||||
{
|
{
|
||||||
setName("Qode Assist");
|
setName("QodeAssist");
|
||||||
LanguageClient::LanguageFilter filter;
|
LanguageClient::LanguageFilter filter;
|
||||||
filter.mimeTypes = QStringList() << "*";
|
filter.mimeTypes = QStringList() << "*";
|
||||||
setSupportedLanguage(filter);
|
setSupportedLanguage(filter);
|
||||||
@@ -58,11 +152,16 @@ QodeAssistClient::QodeAssistClient()
|
|||||||
setupConnections();
|
setupConnections();
|
||||||
|
|
||||||
m_typingTimer.start();
|
m_typingTimer.start();
|
||||||
|
|
||||||
|
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
|
||||||
|
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
QodeAssistClient::~QodeAssistClient()
|
QodeAssistClient::~QodeAssistClient()
|
||||||
{
|
{
|
||||||
cleanupConnections();
|
cleanupConnections();
|
||||||
|
delete m_refactorHoverHandler;
|
||||||
|
delete m_refactorWidgetHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
||||||
@@ -72,6 +171,15 @@ 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,
|
||||||
@@ -80,6 +188,9 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
if (!Settings::codeCompletionSettings().autoCompletion())
|
if (!Settings::codeCompletionSettings().autoCompletion())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (isManualMode())
|
||||||
|
return;
|
||||||
|
|
||||||
auto project = ProjectManager::projectForFile(document->filePath());
|
auto project = ProjectManager::projectForFile(document->filePath());
|
||||||
if (!isEnabled(project))
|
if (!isEnabled(project))
|
||||||
return;
|
return;
|
||||||
@@ -108,26 +219,32 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
|
|||||||
|
|
||||||
QTextCursor cursor = widget->textCursor();
|
QTextCursor cursor = widget->textCursor();
|
||||||
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
|
||||||
QString lastChar = cursor.selectedText();
|
const QString lastChar = cursor.selectedText();
|
||||||
|
if (lastChar.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
|
const QChar lastCh = lastChar[0];
|
||||||
|
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|
||||||
|
|| lastCh == QChar::LineSeparator) {
|
||||||
m_recentCharCount = 0;
|
m_recentCharCount = 0;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool isSpaceOrTab = lastCh.isSpace();
|
||||||
|
const bool ignoreWhitespace
|
||||||
|
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
|
||||||
|
|
||||||
|
if (!ignoreWhitespace || !isSpaceOrTab)
|
||||||
m_recentCharCount += charsAdded;
|
m_recentCharCount += charsAdded;
|
||||||
|
|
||||||
if (m_typingTimer.elapsed()
|
if (m_typingTimer.elapsed()
|
||||||
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
|
||||||
m_recentCharCount = charsAdded;
|
m_recentCharCount = (ignoreWhitespace && isSpaceOrTab) ? 0 : charsAdded;
|
||||||
m_typingTimer.restart();
|
m_typingTimer.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_recentCharCount
|
handleAutoRequestTrigger(widget);
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
|
|
||||||
scheduleRequest(widget);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,15 +260,36 @@ 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 auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
|
editor->abortAssist();
|
||||||
|
|
||||||
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()) {
|
||||||
|
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);
|
||||||
@@ -161,29 +299,66 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
{
|
{
|
||||||
|
if (m_runningRequests.contains(editor)) {
|
||||||
|
if (Settings::codeCompletionSettings().cancelOnInput())
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto it = m_scheduledRequests.find(editor);
|
auto it = m_scheduledRequests.find(editor);
|
||||||
if (it == m_scheduledRequests.end()) {
|
if (it == m_scheduledRequests.end()) {
|
||||||
auto timer = new QTimer(this);
|
auto timer = new QTimer(this);
|
||||||
timer->setSingleShot(true);
|
timer->setSingleShot(true);
|
||||||
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
connect(timer, &QTimer::timeout, this, [this, editor]() {
|
||||||
if (editor
|
if (!editor || m_runningRequests.contains(editor))
|
||||||
&& editor->textCursor().position()
|
return;
|
||||||
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
|
if (editor->textCursor().position()
|
||||||
&& m_recentCharCount
|
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
|
||||||
> Settings::codeCompletionSettings().autoCompletionCharThreshold())
|
return;
|
||||||
requestCompletions(editor);
|
requestCompletions(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
|
||||||
delete m_scheduledRequests.take(editor);
|
delete m_scheduledRequests.take(editor);
|
||||||
cancelRunningRequest(editor);
|
cancelRunningRequest(editor);
|
||||||
});
|
});
|
||||||
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
|
|
||||||
cancelRunningRequest(editor);
|
|
||||||
});
|
|
||||||
it = m_scheduledRequests.insert(editor, timer);
|
it = m_scheduledRequests.insert(editor, timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,20 +368,42 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
|
|||||||
void QodeAssistClient::handleCompletions(
|
void QodeAssistClient::handleCompletions(
|
||||||
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
|
||||||
{
|
{
|
||||||
if (response.error())
|
m_progressHandler.hideProgress();
|
||||||
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
|
||||||
|
editor->abortAssist();
|
||||||
|
|
||||||
|
if (response.error()) {
|
||||||
log(*response.error());
|
log(*response.error());
|
||||||
|
m_errorHandler
|
||||||
|
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
|
||||||
|
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());
|
||||||
|
|
||||||
const MultiTextCursor cursors = editor->multiTextCursor();
|
const MultiTextCursor cursors = editor->multiTextCursor();
|
||||||
if (cursors.hasMultipleCursors())
|
if (cursors.hasMultipleCursors() || cursors.hasSelection())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
|
const int currentPosition = cursors.mainCursor().position();
|
||||||
|
if (requestPosition < 0 || currentPosition < requestPosition)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
QString typedSinceRequest;
|
||||||
|
if (currentPosition > requestPosition) {
|
||||||
|
QTextCursor diffCursor(editor->document());
|
||||||
|
diffCursor.setPosition(requestPosition);
|
||||||
|
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
|
||||||
|
typedSinceRequest = diffCursor.selectedText();
|
||||||
|
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|
||||||
|
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
if (const std::optional<GetCompletionResponse> result = response.result()) {
|
||||||
auto isValidCompletion = [](const Completion &completion) {
|
auto isValidCompletion = [](const Completion &completion) {
|
||||||
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
return completion.isValid() && !completion.text().trimmed().isEmpty();
|
||||||
@@ -214,32 +411,58 @@ void QodeAssistClient::handleCompletions(
|
|||||||
QList<Completion> completions
|
QList<Completion> completions
|
||||||
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
|
||||||
|
|
||||||
// remove trailing whitespaces from the end of the completions
|
QList<Completion> matchedCompletions;
|
||||||
|
matchedCompletions.reserve(completions.size());
|
||||||
for (Completion &completion : completions) {
|
for (Completion &completion : completions) {
|
||||||
const LanguageServerProtocol::Range range = completion.range();
|
const LanguageServerProtocol::Range range = completion.range();
|
||||||
if (range.start().line() != range.end().line())
|
if (range.start().line() != range.end().line())
|
||||||
continue; // do not remove trailing whitespaces for multi-line replacements
|
continue;
|
||||||
|
|
||||||
const QString completionText = completion.text();
|
QString completionText = completion.text();
|
||||||
const int end = int(completionText.size()) - 1; // empty strings have been removed above
|
const int end = int(completionText.size()) - 1;
|
||||||
int delta = 0;
|
int delta = 0;
|
||||||
while (delta <= end && completionText[end - delta].isSpace())
|
while (delta <= end && completionText[end - delta].isSpace())
|
||||||
++delta;
|
++delta;
|
||||||
|
|
||||||
if (delta > 0)
|
if (delta > 0)
|
||||||
completion.setText(completionText.chopped(delta));
|
completionText.chop(delta);
|
||||||
|
|
||||||
|
if (!typedSinceRequest.isEmpty()) {
|
||||||
|
if (!completionText.startsWith(typedSinceRequest))
|
||||||
|
continue;
|
||||||
|
completionText = completionText.mid(typedSinceRequest.size());
|
||||||
|
if (completionText.isEmpty())
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
auto suggestions = Utils::transform(completions, [](const Completion &c) {
|
|
||||||
|
completion.setText(completionText);
|
||||||
|
matchedCompletions.append(completion);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedCompletions.isEmpty()) {
|
||||||
|
LOG_MESSAGE("No valid completions received");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Text::Position anchor = typedSinceRequest.isEmpty()
|
||||||
|
? Text::Position{}
|
||||||
|
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
|
||||||
|
const bool useAnchor = !typedSinceRequest.isEmpty();
|
||||||
|
|
||||||
|
auto suggestions = Utils::transform(matchedCompletions,
|
||||||
|
[useAnchor, &anchor](const Completion &c) {
|
||||||
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
|
||||||
return Text::Position{pos.line() + 1, pos.character()};
|
return Text::Position{pos.line() + 1, pos.character()};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (useAnchor) {
|
||||||
|
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
|
||||||
|
}
|
||||||
|
|
||||||
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
|
||||||
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())
|
|
||||||
return;
|
|
||||||
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,6 +472,7 @@ 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);
|
||||||
}
|
}
|
||||||
@@ -290,4 +514,215 @@ void QodeAssistClient::cleanupConnections()
|
|||||||
m_scheduledRequests.clear();
|
m_scheduledRequests.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
|
||||||
|
{
|
||||||
|
m_progressHandler.hideProgress();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int displayMode = Settings::quickRefactorSettings().displayMode();
|
||||||
|
|
||||||
|
if (displayMode == 0) {
|
||||||
|
displayRefactoringWidget(result);
|
||||||
|
} else {
|
||||||
|
displayRefactoringSuggestion(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
|
||||||
|
{
|
||||||
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
|
||||||
|
{
|
||||||
|
TextEditorWidget *editorWidget = result.editor;
|
||||||
|
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
|
||||||
|
|
||||||
|
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
|
||||||
|
|
||||||
|
QString displayOriginal;
|
||||||
|
QString displayRefactored;
|
||||||
|
QString textToApply = result.newText;
|
||||||
|
|
||||||
|
if (ctx.isInsertion) {
|
||||||
|
bool isMultiline = result.newText.contains('\n');
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
displayOriginal = ctx.textBeforeCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText;
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
|
||||||
|
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
|
||||||
|
textToApply = result.newText;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayOriginal = ctx.originalText;
|
||||||
|
displayRefactored = result.newText;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
|
||||||
|
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
|
||||||
|
});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setDeclineCallback([]() {});
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->showRefactorWidget(
|
||||||
|
editorWidget, displayOriginal, displayRefactored, range,
|
||||||
|
ctx.contextBefore, ctx.contextAfter);
|
||||||
|
|
||||||
|
m_refactorWidgetHandler->setTextToApply(textToApply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &text)
|
||||||
|
{
|
||||||
|
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
|
||||||
|
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
|
||||||
|
const int startPos = startCursor.position();
|
||||||
|
const int endPos = endCursor.position();
|
||||||
|
|
||||||
|
QTextCursor editCursor(editor->document());
|
||||||
|
editCursor.beginEditBlock();
|
||||||
|
|
||||||
|
if (startPos == endPos) {
|
||||||
|
bool isMultiline = text.contains('\n');
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
|
||||||
|
if (isMultiline) {
|
||||||
|
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.insertText(text);
|
||||||
|
} else {
|
||||||
|
editCursor.setPosition(startPos);
|
||||||
|
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
|
||||||
|
editCursor.removeSelectedText();
|
||||||
|
editCursor.insertText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
editCursor.endEditBlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
|
||||||
|
{
|
||||||
|
const QTextCursor cursor = widget->textCursor();
|
||||||
|
const auto &settings = Settings::codeCompletionSettings();
|
||||||
|
const bool smart = settings.smartContextTrigger();
|
||||||
|
|
||||||
|
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
|
||||||
|
return;
|
||||||
|
|
||||||
|
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
|
||||||
|
const int charThreshold = settings.autoCompletionCharThreshold();
|
||||||
|
|
||||||
|
if (eager || m_recentCharCount > charThreshold)
|
||||||
|
scheduleRequest(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
|
||||||
|
if (!editor)
|
||||||
|
return LanguageClient::Client::eventFilter(watched, event);
|
||||||
|
|
||||||
|
if (event->type() == QEvent::KeyPress) {
|
||||||
|
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||||
|
|
||||||
|
if (keyEvent->key() == Qt::Key_Escape) {
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,45 +1,41 @@
|
|||||||
/*
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
* Copyright (C) 2023 The Qt Company Ltd.
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* This file is part of Qode Assist.
|
|
||||||
*
|
|
||||||
* The Qt Company portions:
|
|
||||||
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
|
|
||||||
*
|
|
||||||
* Petr Mironychev portions:
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
#include <languageclient/client.h>
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "LLMClientInterface.hpp"
|
||||||
#include "LSPCompletion.hpp"
|
#include "LSPCompletion.hpp"
|
||||||
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
#include "RefactorSuggestionHoverHandler.hpp"
|
||||||
|
#include "widgets/CompletionProgressHandler.hpp"
|
||||||
|
#include "widgets/CompletionErrorHandler.hpp"
|
||||||
|
#include "widgets/EditorChatButtonHandler.hpp"
|
||||||
|
#include "widgets/RefactorWidgetHandler.hpp"
|
||||||
|
#include <languageclient/client.h>
|
||||||
|
#include <pluginllmcore/IPromptProvider.hpp>
|
||||||
|
#include <pluginllmcore/IProviderRegistry.hpp>
|
||||||
|
|
||||||
namespace QodeAssist {
|
namespace QodeAssist {
|
||||||
|
|
||||||
class QodeAssistClient : public LanguageClient::Client
|
class QodeAssistClient : public LanguageClient::Client
|
||||||
{
|
{
|
||||||
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit QodeAssistClient();
|
explicit QodeAssistClient(LLMClientInterface *clientInterface);
|
||||||
~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);
|
||||||
@@ -50,6 +46,12 @@ private:
|
|||||||
|
|
||||||
void setupConnections();
|
void setupConnections();
|
||||||
void cleanupConnections();
|
void cleanupConnections();
|
||||||
|
void handleRefactoringResult(const RefactorResult &result);
|
||||||
|
void displayRefactoringSuggestion(const RefactorResult &result);
|
||||||
|
void displayRefactoringWidget(const RefactorResult &result);
|
||||||
|
void applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
|
||||||
|
|
||||||
|
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
|
||||||
|
|
||||||
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
|
||||||
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
|
||||||
@@ -58,6 +60,13 @@ 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};
|
||||||
|
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
|
||||||
|
LLMClientInterface *m_llmClient;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace QodeAssist
|
} // namespace QodeAssist
|
||||||
|
|||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!DOCTYPE TS>
|
|
||||||
<TS version="2.1" language="en_001"></TS>
|
|
||||||
@@ -1,21 +1,5 @@
|
|||||||
/*
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
* Copyright (C) 2024 Petr Mironychev
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*
|
|
||||||
* 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
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
424
QuickRefactorHandler.cpp
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#include "QuickRefactorHandler.hpp"
|
||||||
|
|
||||||
|
#include <LLMQore/BaseClient.hpp>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QUuid>
|
||||||
|
|
||||||
|
#include <context/DocumentContextReader.hpp>
|
||||||
|
#include <pluginllmcore/ResponseCleaner.hpp>
|
||||||
|
#include <context/DocumentReaderQtCreator.hpp>
|
||||||
|
#include <context/Utils.hpp>
|
||||||
|
#include <pluginllmcore/PromptTemplateManager.hpp>
|
||||||
|
#include <pluginllmcore/ProvidersManager.hpp>
|
||||||
|
#include <pluginllmcore/RulesLoader.hpp>
|
||||||
|
#include <logger/Logger.hpp>
|
||||||
|
#include <settings/ChatAssistantSettings.hpp>
|
||||||
|
#include <settings/GeneralSettings.hpp>
|
||||||
|
#include <settings/QuickRefactorSettings.hpp>
|
||||||
|
#include <settings/ToolsSettings.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 = PluginLLMCore::ProvidersManager::instance();
|
||||||
|
auto &promptManager = PluginLLMCore::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
QJsonObject payload{
|
||||||
|
{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
|
||||||
|
|
||||||
|
PluginLLMCore::ContextData context = prepareContext(editor, range, instructions);
|
||||||
|
|
||||||
|
bool enableTools = Settings::quickRefactorSettings().useTools();
|
||||||
|
bool enableThinking = Settings::quickRefactorSettings().useThinking();
|
||||||
|
provider->prepareRequest(
|
||||||
|
payload,
|
||||||
|
promptTemplate,
|
||||||
|
context,
|
||||||
|
PluginLLMCore::RequestType::QuickRefactoring,
|
||||||
|
enableTools,
|
||||||
|
enableThinking);
|
||||||
|
|
||||||
|
provider->client()->setMaxToolContinuations(
|
||||||
|
Settings::toolsSettings().maxToolContinuations());
|
||||||
|
|
||||||
|
m_isRefactoringInProgress = true;
|
||||||
|
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestCompleted,
|
||||||
|
this,
|
||||||
|
&QuickRefactorHandler::handleFullResponse,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
|
connect(
|
||||||
|
provider->client(),
|
||||||
|
&::LLMQore::BaseClient::requestFailed,
|
||||||
|
this,
|
||||||
|
&QuickRefactorHandler::handleRequestFailed,
|
||||||
|
Qt::UniqueConnection);
|
||||||
|
|
||||||
|
const QString customEndpoint = Settings::generalSettings().qrCustomEndpoint();
|
||||||
|
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
|
||||||
|
: promptTemplate->endpoint();
|
||||||
|
auto requestId
|
||||||
|
= provider->sendRequest(QUrl(Settings::generalSettings().qrUrl()), payload, endpoint);
|
||||||
|
m_lastRequestId = requestId;
|
||||||
|
QJsonObject request{{"id", requestId}};
|
||||||
|
|
||||||
|
m_activeRequests[requestId] = {request, provider};
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginLLMCore::ContextData QuickRefactorHandler::prepareContext(
|
||||||
|
TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &instructions)
|
||||||
|
{
|
||||||
|
PluginLLMCore::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 = PluginLLMCore::RulesLoader::getActiveProject();
|
||||||
|
if (project) {
|
||||||
|
QString projectRules = PluginLLMCore::RulesLoader::loadRulesForProject(
|
||||||
|
project, PluginLLMCore::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\n# Code Context with Position Markers\n" + taggedContent;
|
||||||
|
|
||||||
|
systemPrompt += "\n\n# Output Requirements\n## What to Generate:";
|
||||||
|
systemPrompt += cursor.hasSelection()
|
||||||
|
? "\n- Generate ONLY the code that should REPLACE the selected text between "
|
||||||
|
"<selection_start> and <selection_end> markers"
|
||||||
|
"\n- Your output will completely replace the selected code"
|
||||||
|
: "\n- Generate ONLY the code that should be INSERTED at the <cursor> position"
|
||||||
|
"\n- Your output will be inserted at the cursor location";
|
||||||
|
|
||||||
|
systemPrompt += "\n\n## Formatting Rules:"
|
||||||
|
"\n- Output ONLY the code itself, without ANY explanations or descriptions"
|
||||||
|
"\n- Do NOT include markdown code blocks (no ```, no language tags)"
|
||||||
|
"\n- Do NOT add comments explaining what you changed"
|
||||||
|
"\n- Do NOT repeat existing code, be precise with context"
|
||||||
|
"\n- Do NOT send in answer <cursor> or </cursor> and other tags"
|
||||||
|
"\n- The output must be ready to insert directly into the editor as-is";
|
||||||
|
|
||||||
|
systemPrompt += "\n\n## Indentation and Whitespace:";
|
||||||
|
|
||||||
|
if (cursor.hasSelection()) {
|
||||||
|
QTextBlock startBlock = documentInfo.document->findBlock(cursor.selectionStart());
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : startBlock.text()) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: The code to replace starts with %1 spaces of indentation"
|
||||||
|
"\n- Your output MUST start with exactly %1 spaces (or equivalent tabs)"
|
||||||
|
"\n- Each line in your output must maintain this base indentation")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
systemPrompt += "\n- PRESERVE all indentation from the original code";
|
||||||
|
} else {
|
||||||
|
QTextBlock block = documentInfo.document->findBlock(cursorPos);
|
||||||
|
QString lineText = block.text();
|
||||||
|
int leadingSpaces = 0;
|
||||||
|
for (QChar c : lineText) {
|
||||||
|
if (c == ' ') leadingSpaces++;
|
||||||
|
else if (c == '\t') leadingSpaces += 4;
|
||||||
|
else break;
|
||||||
|
}
|
||||||
|
if (leadingSpaces > 0) {
|
||||||
|
systemPrompt += QString("\n- CRITICAL: Current line has %1 spaces of indentation"
|
||||||
|
"\n- If generating multiline code, EVERY line must start with at least %1 spaces"
|
||||||
|
"\n- If generating single-line code, it will be inserted inline (no indentation needed)")
|
||||||
|
.arg(leadingSpaces);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
systemPrompt += "\n- Use the same indentation style (spaces or tabs) as the surrounding code"
|
||||||
|
"\n- Maintain consistent indentation for nested blocks"
|
||||||
|
"\n- Do NOT remove or reduce the base indentation level"
|
||||||
|
"\n\n## Code Style:"
|
||||||
|
"\n- Match the coding style of the surrounding code (naming, spacing, braces, etc.)"
|
||||||
|
"\n- Preserve the original code structure when possible"
|
||||||
|
"\n- Only change what is necessary to fulfill the user's request";
|
||||||
|
|
||||||
|
if (Settings::quickRefactorSettings().useOpenFilesInQuickRefactor()) {
|
||||||
|
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
|
||||||
|
}
|
||||||
|
|
||||||
|
context.systemPrompt = systemPrompt;
|
||||||
|
|
||||||
|
QVector<PluginLLMCore::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 = PluginLLMCore::ResponseCleaner::clean(response);
|
||||||
|
|
||||||
|
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
|
||||||
74
QuickRefactorHandler.hpp
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
#include <context/ContextManager.hpp>
|
||||||
|
#include <context/IDocumentReader.hpp>
|
||||||
|
#include <pluginllmcore/ContextData.hpp>
|
||||||
|
#include <pluginllmcore/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);
|
||||||
|
PluginLLMCore::ContextData prepareContext(
|
||||||
|
TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
const QString &instructions);
|
||||||
|
|
||||||
|
struct RequestContext
|
||||||
|
{
|
||||||
|
QJsonObject originalRequest;
|
||||||
|
PluginLLMCore::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
|
||||||
619
README.md
@@ -1,11 +1,12 @@
|
|||||||
# QodeAssist - AI-powered coding assistant plugin for Qt Creator
|
# QodeAssist — AI coding assistant for Qt Creator
|
||||||
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
[](https://discord.gg/BGMkUsXUgf)
|
|
||||||
|
|
||||||
 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.
|
[](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
|
||||||
|
[](https://github.com/Palm1r/QodeAssist/releases)
|
||||||
|

|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||||
|
[](https://discord.gg/BGMkUsXUgf)
|
||||||
|
|
||||||
|
 **QodeAssist** brings a full AI coding workflow to Qt Creator for C++ and QML — smart code completion, multi-panel chat, inline quick refactoring, and project-aware tool calling. It works with local runtimes (Ollama, llama.cpp, LM Studio) and cloud providers (Claude, OpenAI, Google AI, Mistral), can run as an **MCP server** so other clients reuse its project context, and can also act as an **MCP client** to consume tools from external MCP servers (authenticated MCP servers are not supported yet).
|
||||||
|
|
||||||
⚠️ **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:
|
||||||
@@ -13,61 +14,45 @@
|
|||||||
> - 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 to QtCreator](#install-plugin-to-qtcreator)
|
2. [Install Plugin](#install-plugin-to-qtcreator)
|
||||||
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
|
3. [Configuration](#configuration)
|
||||||
4. [Configure for OpenAI](#configure-for-openai)
|
4. [Features](#features)
|
||||||
4. [Configure for Mistral AI](#configure-for-mistral-ai)
|
5. [Context Layers](#context-layers)
|
||||||
4. [Configure for Google AI](#configure-for-google-ai)
|
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
|
||||||
5. [Configure for Ollama](#configure-for-ollama)
|
7. [Hotkeys](#hotkeys)
|
||||||
6. [System Prompt Configuration](#system-prompt-configuration)
|
8. [Troubleshooting](#troubleshooting)
|
||||||
7. [File Context Features](#file-context-features)
|
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
|
||||||
|
|
||||||
- AI-powered code completion
|
QodeAssist enhances Qt Creator with AI-powered coding assistance:
|
||||||
- Chat functionality:
|
|
||||||
- Side and Bottom panels
|
|
||||||
- Chat history autosave and restore
|
|
||||||
- Token usage monitoring and management
|
|
||||||
- Attach files for one-time code analysis
|
|
||||||
- Link files for persistent context with auto update in conversations
|
|
||||||
- Automatic syncing with open editor files (optional)
|
|
||||||
- Support for multiple LLM providers:
|
|
||||||
- Ollama
|
|
||||||
- OpenAI
|
|
||||||
- Anthropic Claude
|
|
||||||
- LM Studio
|
|
||||||
- Mistral AI
|
|
||||||
- Google AI
|
|
||||||
- OpenAI-compatible providers(eg. llama.cpp, https://openrouter.ai)
|
|
||||||
- Extensive library of model-specific templates
|
|
||||||
- Custom template support
|
|
||||||
- Easy configuration and model selection
|
|
||||||
|
|
||||||
Join our Discord Community: Have questions or want to discuss QodeAssist? Join our [Discord server](https://discord.gg/BGMkUsXUgf) to connect with other users and get support!
|
- **Code Completion** — intelligent, context-aware suggestions (FIM and chat models) for C++ and QML, with multiline support
|
||||||
|
- **Chat Assistant** — side panel, bottom panel, or detached window; history with auto-save, token monitoring, extended thinking
|
||||||
|
- **Quick Refactoring** — inline AI-assisted edits directly in the editor with a searchable custom-instructions library
|
||||||
|
- **Agent Tools** — read, search, create and edit files; build the project; run terminal commands; access linter/compiler issues; manage TODOs
|
||||||
|
- **MCP Server** — expose QodeAssist's project-aware tools to external MCP clients (Claude Code, VS Code, Claude Desktop via bridge)
|
||||||
|
- **MCP Client Hub** — connect QodeAssist to external MCP servers and use their tools in Chat and Quick Refactor (authenticated MCP servers are not supported yet)
|
||||||
|
- **File Context** — attach, link, or auto-sync open editor files for richer prompts
|
||||||
|
- **Many Providers** — Ollama, llama.cpp, LM Studio (Chat + Responses), Claude, OpenAI (Chat + Responses), Google AI, Mistral, Codestral, OpenRouter, any OpenAI-compatible endpoint
|
||||||
|
- **Customizable** — per-project rules (`.qodeassist/rules/`), agent roles, reusable refactor templates, full prompt-template control
|
||||||
|
|
||||||
|
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
|
||||||
|
|
||||||
<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">
|
||||||
@@ -83,15 +68,58 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
|
|||||||
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
|
<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...
|
||||||
@@ -99,179 +127,338 @@ Join our Discord Community: Have questions or want to discuss QodeAssist? Join o
|
|||||||
- Click on "Install Plugin..."
|
- Click on "Install Plugin..."
|
||||||
- Select the downloaded QodeAssist plugin archive file
|
- Select the downloaded QodeAssist plugin archive file
|
||||||
|
|
||||||
## Configure for Anthropic Claude
|
## Configuration
|
||||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
|
||||||
2. Go to Provider Settings tab and configure Claude api key
|
### Quick Setup (Recommended for Beginners)
|
||||||
3. Return to General tab and configure:
|
|
||||||
- Set "Claude" as the provider for code completion or/and chat assistant
|
The Quick Setup feature provides one-click configuration for popular cloud AI models. Get started in 3 easy steps:
|
||||||
- Set the Claude URL (https://api.anthropic.com)
|
|
||||||
- Select your preferred model (e.g., claude-3-5-sonnet-20241022)
|
|
||||||
- Choose the Claude template for code completion or/and chat
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example of Claude settings: (click to expand)</summary>
|
<summary>Quick setup: (click to expand)</summary>
|
||||||
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
|
<img width="600" alt="Quick Setup" src="https://github.com/user-attachments/assets/20df9155-9095-420c-8387-908bd931bcfa">
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Configure for OpenAI
|
1. **Open QodeAssist Settings**
|
||||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
2. **Select a Preset** - Choose from the Quick Setup dropdown:
|
||||||
2. Go to Provider Settings tab and configure OpenAI api key
|
- **Anthropic Claude** (Sonnet 4.5, Haiku 4.5, Opus 4.5)
|
||||||
3. Return to General tab and configure:
|
- **OpenAI** (gpt-5.2-codex)
|
||||||
- Set "OpenAI" as the provider for code completion or/and chat assistant
|
- **Mistral AI** (Codestral 2501)
|
||||||
- Set the OpenAI URL (https://api.openai.com)
|
- **Google AI** (Gemini 2.5 Flash)
|
||||||
- Select your preferred model (e.g., gpt-4o)
|
3. **Configure API Key** - Click "Configure API Key" button and enter your API key in Provider Settings
|
||||||
- Choose the OpenAI template for code completion or/and chat
|
|
||||||
|
All settings (provider, model, template, URL) are configured automatically. Just add your API key and you're ready to go!
|
||||||
|
|
||||||
|
### Manual Provider Configuration
|
||||||
|
|
||||||
|
For advanced users or local models, choose your preferred provider and follow the detailed configuration guide:
|
||||||
|
|
||||||
|
**Local providers:**
|
||||||
|
- **[Ollama](docs/ollama-configuration.md)** — native Ollama API
|
||||||
|
- **Ollama (OpenAI-compatible)** — Ollama's `/v1` endpoint for tool-calling models
|
||||||
|
- **[llama.cpp](docs/llamacpp-configuration.md)** — local `llama-server`
|
||||||
|
- **LM Studio** — OpenAI-compatible Chat API
|
||||||
|
- **LM Studio (Responses API)** — newer models that require the Responses endpoint
|
||||||
|
|
||||||
|
**Cloud providers:**
|
||||||
|
- **[Anthropic Claude](docs/claude-configuration.md)** — manual setup guide
|
||||||
|
- **[OpenAI](docs/openai-configuration.md)** — Chat Completions and Responses API
|
||||||
|
- **[Mistral AI](docs/mistral-configuration.md)** / **Codestral**
|
||||||
|
- **[Google AI](docs/google-ai-configuration.md)** — Gemini
|
||||||
|
- **OpenAI-compatible** — OpenRouter and any custom endpoint
|
||||||
|
|
||||||
|
### Recommended Models for Best Experience
|
||||||
|
|
||||||
|
For optimal coding assistance, we recommend using these top-tier models:
|
||||||
|
|
||||||
|
**Best for Code Completion & Refactoring:**
|
||||||
|
- **Claude 4.5 Haiku or Sonnet** (Anthropic)
|
||||||
|
- **GPT-5.1-codex or codex-mini** (OpenAI Responses API)
|
||||||
|
- **Codestral** (Mistral)
|
||||||
|
|
||||||
|
**Best for Chat Assistant:**
|
||||||
|
- **Claude 4.5 Sonnet** (Anthropic) - Outstanding reasoning and natural conversation flow
|
||||||
|
- **GPT-5.1-codex** (OpenAI Responses API) - Latest model with advanced capabilities
|
||||||
|
- **Gemini 2.5 or 3.0** (Google AI) - Latest models from Google
|
||||||
|
- **Mistral large** (Mistral) - Fast and capable
|
||||||
|
|
||||||
|
**Local models:**
|
||||||
|
- **Qwen3-coder** (Qwen) - Best local models
|
||||||
|
|
||||||
|
### Additional Configuration
|
||||||
|
|
||||||
|
- **[Agent Roles](docs/agent-roles.md)** - Create AI personas with specialized system prompts
|
||||||
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress conversations to save context tokens
|
||||||
|
- **[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
|
||||||
|
|
||||||
|
#### Completion Trigger Modes
|
||||||
|
|
||||||
|
QodeAssist offers two trigger modes for code completion:
|
||||||
|
|
||||||
|
**Hint-based**
|
||||||
|
- Shows a hint indicator near cursor when you type 3+ characters
|
||||||
|
- Press **Space** (or custom key) to request completion
|
||||||
|
- **Best for**: Paid APIs (Claude, OpenAI), conscious control
|
||||||
|
- **Benefits**: No unexpected API charges, full control over requests, no workflow interruption
|
||||||
|
- **Visual**: Clear indicator shows when completion is ready
|
||||||
|
|
||||||
|
**Automatic(Default)**
|
||||||
|
- Automatically requests completion after typing threshold
|
||||||
|
- Works immediately without additional keypresses
|
||||||
|
- **Best for**: Local models (Ollama, llama.cpp), maximum automation
|
||||||
|
- **Benefits**: Hands-free experience, instant suggestions
|
||||||
|
|
||||||
|
💡 **Tip**: Start with Hint-based to avoid unexpected costs. Switch to Automatic if using free local models.
|
||||||
|
|
||||||
|
Configure in: `Tools → Options → QodeAssist → Code Completion → General Settings`
|
||||||
|
|
||||||
|
### Chat Assistant
|
||||||
|
- Multiple chat panels: side panel, bottom panel, and popup window
|
||||||
|
- Chat history with auto-save and restore
|
||||||
|
- Token usage monitoring
|
||||||
|
- **[Agent Roles](docs/agent-roles.md)** - Switch between AI personas (Developer, Reviewer, custom roles)
|
||||||
|
- **[Chat Summarization](docs/chat-summarization.md)** - Compress long conversations into AI-generated summaries
|
||||||
|
- **[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
|
||||||
|
- **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
|
||||||
|
|
||||||
|
Chat and Quick Refactor can call tools to inspect and modify your project. Each tool can be individually enabled/disabled in settings.
|
||||||
|
|
||||||
|
| Tool | What it does |
|
||||||
|
|------|--------------|
|
||||||
|
| `list_project_files` | List files in the active project(s) |
|
||||||
|
| `find_file` | Find a file by name or partial path |
|
||||||
|
| `read_file` | Read file contents (project or absolute path) |
|
||||||
|
| `search_project` | Grep / symbol search across project sources |
|
||||||
|
| `create_new_file` | Create a new empty file on disk |
|
||||||
|
| `edit_file` | Replace content in a file (old → new) |
|
||||||
|
| `build_project` | Build the active project and return compiler output |
|
||||||
|
| `get_issues_list` | Read current linter / compiler diagnostics |
|
||||||
|
| `execute_terminal_command` | Run a shell command (with confirmation) |
|
||||||
|
| `todo_tool` | Track multi-step task progress during a conversation |
|
||||||
|
|
||||||
|
### MCP Server
|
||||||
|
|
||||||
|
QodeAssist can run an **MCP (Model Context Protocol) server** on `localhost`, exposing the tools above to external clients — so you can use QodeAssist's project awareness from Claude Code CLI, VS Code, Cursor, Claude Desktop, or any other MCP-capable client.
|
||||||
|
|
||||||
|
- **Enable** in `Tools → Options → QodeAssist → MCP Server`
|
||||||
|
- **Transport**: HTTP + SSE by default; a stdio bridge is provided for clients that only speak stdio (e.g. Claude Desktop)
|
||||||
|
- **Ready-to-copy snippets** for Claude Code, VS Code, and the bridge are available via the "Show connection instructions" button in settings
|
||||||
|
|
||||||
|
### MCP Client Hub
|
||||||
|
|
||||||
|
QodeAssist can also act as an **MCP client**, connecting to external MCP servers and making their tools available to Chat and Quick Refactor alongside the built-in ones.
|
||||||
|
|
||||||
|
- **Configure** servers in `Tools → Options → QodeAssist → MCP Client`
|
||||||
|
- **Transports**: stdio and HTTP/SSE
|
||||||
|
- **Limitation**: authenticated MCP servers (OAuth / token-protected) are **not supported yet** — only servers that accept unauthenticated local connections work for now
|
||||||
|
|
||||||
|
## 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>Example of OpenAI settings: (click to expand)</summary>
|
<summary><strong>Code Completion (FIM Models)</strong> - Codestral, Qwen2.5-Coder, DeepSeek-Coder (click to expand)</summary>
|
||||||
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 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 Mistral AI
|
|
||||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
|
||||||
2. Go to Provider Settings tab and configure Mistral AI api key
|
|
||||||
3. Return to General tab and configure:
|
|
||||||
- Set "Mistral AI" as the provider for code completion or/and chat assistant
|
|
||||||
- Set the OpenAI URL (https://api.mistral.ai)
|
|
||||||
- Select your preferred model (e.g., mistral-large-latest)
|
|
||||||
- Choose the Mistral AI template for code completion or/and chat
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example of Mistral AI settings: (click to expand)</summary>
|
<summary><strong>Code Completion (Non-FIM Models)</strong> - DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct (click to expand)</summary>
|
||||||
<img width="829" alt="Mistral AI Settings" src="https://github.com/user-attachments/assets/1c5ed13b-a29b-43f7-b33f-2e05fdea540c" />
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 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 Google AI
|
|
||||||
1. Open Qt Creator settings and navigate to the QodeAssist section
|
|
||||||
2. Go to Provider Settings tab and configure Google AI api key
|
|
||||||
3. Return to General tab and configure:
|
|
||||||
- Set "Google AI" as the provider for code completion or/and chat assistant
|
|
||||||
- Set the OpenAI URL (https://generativelanguage.googleapis.com/v1beta)
|
|
||||||
- Select your preferred model (e.g., gemini-2.0-flash)
|
|
||||||
- Choose the Google AI template
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Example of Google AI settings: (click to expand)</summary>
|
<summary><strong>Chat Assistant</strong> - Interactive coding assistant (click to expand)</summary>
|
||||||
<img width="829" alt="Google AI Settings" src="https://github.com/user-attachments/assets/046ede65-a94d-496c-bc6c-41f3750be12a" />
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ CHAT ASSISTANT │
|
||||||
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ 1. System Prompt (from Chat Assistant Settings) │
|
||||||
|
│ 2. Agent Role (optional, from role selector): │
|
||||||
|
│ └─ Role-specific system prompt (Developer, Reviewer, custom) │
|
||||||
|
│ 3. Project Rules: │
|
||||||
|
│ ├─ .qodeassist/rules/common/*.md │
|
||||||
|
│ └─ .qodeassist/rules/chat/*.md │
|
||||||
|
│ 4. File Context (optional): │
|
||||||
|
│ ├─ Attached files (manual) │
|
||||||
|
│ ├─ Linked files (persistent) │
|
||||||
|
│ └─ Open editor files (if auto-sync enabled) │
|
||||||
|
│ 5. Tool Definitions (if enabled): │
|
||||||
|
│ ├─ ReadProjectFileByName │
|
||||||
|
│ ├─ ListProjectFiles │
|
||||||
|
│ ├─ SearchInProject │
|
||||||
|
│ └─ GetIssuesList │
|
||||||
|
│ 6. Conversation History │
|
||||||
|
│ 7. User Message │
|
||||||
|
│ │
|
||||||
|
│ Final Prompt: [System: SystemPrompt + AgentRole + Rules + Tools] │
|
||||||
|
│ [History: Previous messages] │
|
||||||
|
│ [User: FileContext + UserMessage] │
|
||||||
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Configure for 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>Example of Ollama settings: (click to expand)</summary>
|
<summary><strong>Quick Refactoring</strong> - Inline code improvements (click to expand)</summary>
|
||||||
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 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>
|
</details>
|
||||||
|
|
||||||
## System Prompt Configuration
|
### Key Points
|
||||||
|
|
||||||
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
|
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
|
||||||
|
- **System Prompts** are configured independently for each feature in Settings
|
||||||
|
- **Agent Roles** add role-specific prompts on top of the base system prompt (Chat only)
|
||||||
|
- **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
|
||||||
|
|
||||||
## File Context Features
|
See [Project Rules Documentation](docs/project-rules.md), [Agent Roles Guide](docs/agent-roles.md), and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
### Attached Files
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
## QtCreator Version Compatibility
|
## QtCreator Version Compatibility
|
||||||
|
|
||||||
- QtCreator 15.0.1 - 0.4.8 - 0.5.x
|
| Qt Creator Version | QodeAssist Version |
|
||||||
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
|
|-------------------|-------------------|
|
||||||
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
|
| 17.0.0+ | 0.6.0 - 0.x.x |
|
||||||
- QtCreator 14.0.1 - 0.2.2 plugin version and below
|
| 16.0.2 | 0.5.13 - 0.9.6 |
|
||||||
|
| 16.0.1 | 0.5.7 - 0.5.13 |
|
||||||
## Development Progress
|
| 16.0.0 | 0.5.2 - 0.5.6 |
|
||||||
|
| 15.0.1 | 0.4.8 - 0.5.1 |
|
||||||
- [x] Basic plugin with code autocomplete functionality
|
| 15.0.0 | 0.4.0 - 0.4.7 |
|
||||||
- [x] Improve and automate settings
|
| 14.0.2 | 0.2.3 - 0.3.x |
|
||||||
- [x] Add chat functionality
|
| 14.0.1 | ≤ 0.2.2 |
|
||||||
- [x] Sharing diff with model
|
|
||||||
- [ ] Sharing project source with model
|
|
||||||
- [ ] Support for more providers and models
|
|
||||||
|
|
||||||
## Hotkeys
|
## Hotkeys
|
||||||
|
|
||||||
- To call manual request to suggestion, you can use or change it in settings
|
All hotkeys can be customized in Qt Creator Settings. Default hotkeys:
|
||||||
- on Mac: Option + Command + Q
|
|
||||||
- on Windows: Ctrl + Alt + Q
|
| Action | macOS | Windows/Linux |
|
||||||
- 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
|
| 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
|
## Troubleshooting
|
||||||
|
|
||||||
If QodeAssist is having problems connecting to the LLM provider, please check the following:
|
Having issues with QodeAssist? Check our [detailed troubleshooting guide](docs/troubleshooting.md) for:
|
||||||
|
|
||||||
1. Verify the IP address and port:
|
- Connection issues and provider URLs
|
||||||
|
- Model and template compatibility
|
||||||
|
- Platform-specific issues (Linux, macOS, Windows)
|
||||||
|
- Resetting settings to defaults
|
||||||
|
- Common problems and solutions
|
||||||
|
|
||||||
- For Ollama, the default is usually http://localhost:11434
|
For additional support, join our [Discord Community](https://discord.gg/BGMkUsXUgf) or check [GitHub Issues](https://github.com/Palm1r/QodeAssist/issues).
|
||||||
- For LM Studio, the default is usually http://localhost:1234
|
|
||||||
|
|
||||||
2. Check the endpoint:
|
## Development Progress
|
||||||
|
|
||||||
Make sure the endpoint in the settings matches the one required by your provider
|
- [x] Code completion (FIM and chat models)
|
||||||
- For Ollama, it should be /api/generate
|
- [x] Chat assistant (side / bottom / detached panels)
|
||||||
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
|
- [x] Quick refactoring with custom-instructions library
|
||||||
|
- [x] Diff sharing with models
|
||||||
3. Confirm that the selected model and template are compatible:
|
- [x] Tools / function calling (file I/O, build, terminal, diagnostics)
|
||||||
|
- [x] Project-specific rules (`.qodeassist/rules/`)
|
||||||
Ensure you've chosen the correct model in the "Select Models" option
|
- [x] MCP (Model Context Protocol) — QodeAssist as a server
|
||||||
Verify that the selected prompt template matches the model you're using
|
- [x] MCP — QodeAssist as a client (consume external MCP tools; authenticated MCP servers not yet supported)
|
||||||
|
- [ ] Full project source sharing
|
||||||
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
|
- [ ] Additional provider support
|
||||||
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:
|
||||||
@@ -290,22 +477,50 @@ If you find QodeAssist helpful, there are several ways you can support the proje
|
|||||||
|
|
||||||
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
|
Every contribution, no matter how small, is greatly appreciated and helps keep the project alive!
|
||||||
|
|
||||||
|
## Related Projects
|
||||||
|
|
||||||
|
- **[LLMQore](https://github.com/Palm1r/llmqore)** — the standalone LLM-core library extracted from QodeAssist, reusable in other Qt/C++ projects
|
||||||
|
- **[QodeAssistUpdater](https://github.com/Palm1r/QodeAssistUpdater)** — CLI installer/updater for the plugin
|
||||||
|
|
||||||
## How to Build
|
## How to Build
|
||||||
|
|
||||||
Create a build directory and run
|
### Prerequisites
|
||||||
|
- CMake 3.16+
|
||||||
|
- C++20 compatible compiler
|
||||||
|
- Qt Creator development files
|
||||||
|
|
||||||
cmake -DCMAKE_PREFIX_PATH=<path_to_qtcreator> -DCMAKE_BUILD_TYPE=RelWithDebInfo <path_to_plugin_source>
|
### Build Steps
|
||||||
cmake --build .
|
|
||||||
|
|
||||||
where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator build directory, or to a
|
1. Create a build directory:
|
||||||
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
|
|
||||||
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
|
```bash
|
||||||
relative or absolute path to this plugin directory.
|
mkdir build && cd build
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
|
### Code Style
|
||||||
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).
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|||||||
99
RefactorContextHelper.hpp
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QTextCursor>
|
||||||
|
#include <QTextBlock>
|
||||||
|
|
||||||
|
#include <texteditor/texteditor.h>
|
||||||
|
#include <utils/textutils.h>
|
||||||
|
|
||||||
|
namespace QodeAssist {
|
||||||
|
|
||||||
|
struct RefactorContext
|
||||||
|
{
|
||||||
|
QString originalText;
|
||||||
|
QString textBeforeCursor;
|
||||||
|
QString textAfterCursor;
|
||||||
|
QString contextBefore;
|
||||||
|
QString contextAfter;
|
||||||
|
int startPos{0};
|
||||||
|
int endPos{0};
|
||||||
|
bool isInsertion{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
class RefactorContextHelper
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static RefactorContext extractContext(TextEditor::TextEditorWidget *editor,
|
||||||
|
const Utils::Text::Range &range,
|
||||||
|
int contextLinesBefore = 3,
|
||||||
|
int contextLinesAfter = 3)
|
||||||
|
{
|
||||||
|
RefactorContext ctx;
|
||||||
|
if (!editor) {
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextDocument *doc = editor->document();
|
||||||
|
ctx.startPos = range.begin.toPositionInDocument(doc);
|
||||||
|
ctx.endPos = range.end.toPositionInDocument(doc);
|
||||||
|
ctx.isInsertion = (ctx.startPos == ctx.endPos);
|
||||||
|
|
||||||
|
if (!ctx.isInsertion) {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.setPosition(ctx.endPos, QTextCursor::KeepAnchor);
|
||||||
|
ctx.originalText = cursor.selectedText();
|
||||||
|
ctx.originalText.replace(QChar(0x2029), "\n");
|
||||||
|
} else {
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
|
||||||
|
int posInBlock = cursor.positionInBlock();
|
||||||
|
cursor.movePosition(QTextCursor::StartOfBlock);
|
||||||
|
cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, posInBlock);
|
||||||
|
ctx.textBeforeCursor = cursor.selectedText();
|
||||||
|
|
||||||
|
cursor.setPosition(ctx.startPos);
|
||||||
|
cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
|
||||||
|
ctx.textAfterCursor = cursor.selectedText();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.contextBefore = extractContextLines(doc, ctx.startPos, contextLinesBefore, true);
|
||||||
|
ctx.contextAfter = extractContextLines(doc, ctx.endPos, contextLinesAfter, false);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QString extractContextLines(QTextDocument *doc, int position, int lineCount, bool before)
|
||||||
|
{
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
cursor.setPosition(position);
|
||||||
|
QTextBlock currentBlock = cursor.block();
|
||||||
|
|
||||||
|
QStringList lines;
|
||||||
|
|
||||||
|
if (before) {
|
||||||
|
QTextBlock block = currentBlock.previous();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.prepend(block.text());
|
||||||
|
block = block.previous();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
QTextBlock block = currentBlock.next();
|
||||||
|
for (int i = 0; i < lineCount && block.isValid(); ++i) {
|
||||||
|
lines.append(block.text());
|
||||||
|
block = block.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace QodeAssist
|
||||||
|
|
||||||
186
RefactorSuggestion.cpp
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
27
RefactorSuggestion.hpp
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Copyright (C) 2024-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
194
RefactorSuggestionHoverHandler.cpp
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
56
RefactorSuggestionHoverHandler.hpp
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (C) 2025-2026 Petr Mironychev
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
#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
|
||||||
|
|
||||||
14
TaskFlow/CMakeLists.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
|
)
|
||||||
41
TaskFlow/Editor/CMakeLists.txt
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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}
|
||||||
|
)
|
||||||