Compare commits

...

314 Commits

Author SHA1 Message Date
0aed886e31 feat: Add refactor editor widget 2025-11-23 13:14:44 +01:00
07de415346 feat: Add execution command tool (#273) 2025-11-23 12:52:20 +01:00
a15f64a234 feat: Improve build project tool (#272) 2025-11-22 13:15:15 +01:00
0feaa3a0f7 chore: Upgrade plugin version to 0.9.1 2025-11-20 23:26:06 +01:00
a3527e1442 fix: Remove image dublicate (#271) 2025-11-20 23:25:00 +01:00
24565dc81f chore: Move settings it setups 2025-11-20 18:04:55 +01:00
90655cded4 feat: Add settings for ignore space and tab in codecompletion char count 2025-11-20 17:37:00 +01:00
1e3b1997cc feat: Add configuration manager (#270) 2025-11-20 17:31:13 +01:00
6f7d8a0987 feat: Add drag n drop for chat (#269)
feat: Add dran n drop for chat
2025-11-20 16:19:50 +01:00
55b6080273 feat: Add image support for Claude, OpenAI and Google (#268)
* feat: Add image support for Claude
* feat: Add images support for OpenAI
* feat: Add support images for google ai
* refactor: Separate ImageComponent
* feat: Add attach image button
* feat: Add support image for Mistral provider
* feat: Add support images for OpenAI compatible providers
* feat: Add support images for Ollama
2025-11-20 15:49:39 +01:00
ce9e2717d6 refactor: Change default code completion type to auto 2025-11-19 01:19:50 +01:00
ef73895823 feat: Add open in editor and remove from list (#267)
* refactor: Rename and move chat items
* feat: Add hotkeys for open in editor and remove file from list
* feat: Add opening by system
* feat: Add context action menu
2025-11-19 01:15:43 +01:00
bcdec96d92 fear: Add hint-trigger for call code completion (#266) 2025-11-17 22:24:04 +01:00
86c6930c5f chore: Update plugin to 0.9.0 2025-11-17 15:56:00 +01:00
f3aa706227 feat: Add default option for custom instructions 2025-11-17 15:54:35 +01:00
944e7fb00a fix: Set default focus on custom commands in quick refactor dialog 2025-11-17 15:44:55 +01:00
296a0ff7b8 fix: Chagne progress widget icon (#259)
fix: add manual icon
2025-11-17 15:19:25 +01:00
06bd7db7ea chore: Remove extra comments 2025-11-17 14:39:59 +01:00
43f9e4e75b doc: Add context layers to README 2025-11-17 14:17:10 +01:00
204cffd7d0 feat: Add custom instructions for quick refactor (#258)
* feat: Add custom commands to quick refactor

* doc: Update for quick refactor feature
2025-11-17 13:53:46 +01:00
995597d789 fix: Filter empty thinking blocks for google ai 2025-11-17 11:35:38 +01:00
9974b2f5e6 feat: Run for quick refactoring only reading context tools 2025-11-17 11:25:42 +01:00
31e3d9db7c feat: Add cancel function for progress indicator 2025-11-16 16:14:12 +01:00
6f680e3974 fix: Remove extra logs 2025-11-15 14:55:22 +01:00
953774aaa8 refactor: Full rework quick refactor (#257) 2025-11-15 14:51:47 +01:00
9ecd285d1d feat: Improve code suggestions (#256) 2025-11-14 17:02:43 +01:00
0ca1decd97 fix: Update plugin to 0.8.3 2025-11-14 02:11:15 +01:00
baf129f0dc feat: Add download button for updater 2025-11-14 02:00:33 +01:00
8570b9667a refactor: Change handling finished tools and thinking blocks 2025-11-14 01:46:36 +01:00
f5a445b021 feat: Add updater to release folder 2025-11-14 00:30:38 +01:00
30885c0373 feat: Add google provider thinking mode (#255)
fix: add signature
2025-11-13 23:52:38 +01:00
5e580b8792 chore: Update plugin to 0.8.2 2025-11-13 02:31:05 +01:00
5352fd4f0b doc: Add thinking mode to README 2025-11-13 02:28:38 +01:00
6e9db1552c fix: Replace text to icon in Reset button 2025-11-13 02:23:45 +01:00
c302138568 feat: Add disappearing tools and thinking component from chat 2025-11-13 02:19:49 +01:00
75cbc46808 fix: Add write settings after changing 2025-11-13 01:20:50 +01:00
1070de6e6e fix: Improve edit file tool description 2025-11-13 01:14:14 +01:00
b3432cd76f fix: Clean resources after finish request 2025-11-13 00:56:41 +01:00
f99e4aefb0 refactor: Change opening url in chat by mouse 2025-11-13 00:56:13 +01:00
191de10926 refactor: Remove common project rules from code completion 2025-11-13 00:55:20 +01:00
8492ef29b2 fix: Text color in rules viewer 2025-11-13 00:40:10 +01:00
fea9ecddc8 fix: Remove replace message after complete receiving 2025-11-12 20:32:01 +01:00
a26d475806 fix: Add active project name and path to system prompt 2025-11-12 19:31:12 +01:00
1cd19aa5d1 feat: Add open link to text block context menu 2025-11-12 19:18:21 +01:00
4956d6ab7d feat: Add context menu to edit item 2025-11-12 19:06:29 +01:00
2d92b8fa53 fix: Change params for gpt-5 and o- models
only for chat\completions models
2025-11-12 18:41:51 +01:00
161d77ac04 feat: Add Claude extended thinking (#254)
* feat: Add Claude extended thinking
* fix: Set 1.0 temperature for thinking mode
2025-11-12 18:33:15 +01:00
89797639cf fix: Getting http errors (#252) 2025-11-11 09:00:33 +01:00
4d79760481 doc: Remove qtc badges 2025-11-11 00:45:12 +01:00
3be70556ec doc: Move out docs from main README 2025-11-10 21:16:58 +01:00
a1ff17eef0 doc: Add info about QodeAssistUpdater 2025-11-10 20:52:45 +01:00
6c1d9ddc0e Upgrade plugin to 0.8.1 2025-11-03 18:07:29 +01:00
6ff6901421 fix: Remove check empty file for edit tool 2025-11-03 18:06:51 +01:00
a2f3ae4f64 fix: Changes to top of file 2025-11-03 15:01:51 +01:00
6937b48fbf fix: Creating dir for new file 2025-11-03 13:56:25 +01:00
c6e77c59d3 refactor: remove hardcoded tools guildelines 2025-11-03 10:07:47 +01:00
655471aec6 chore: Upgrade plugin to 0.8.0 2025-11-03 09:14:47 +01:00
af90d3cad2 refactor: Move build tools to experimental tools 2025-11-03 09:01:20 +01:00
9b90aaa06e feat: Add edit file tool (#249)
* feat: Add edit file tool
* feat: Add icons for action buttons
2025-11-03 08:56:52 +01:00
e7110810f8 fix: Clear connection before cancel 2025-11-01 20:51:01 +01:00
1848d44503 fix: Chat mode default value 2025-10-31 23:23:08 +01:00
db82fb08e8 feat: Add chat-agent switcher in chat ui (#247)
* feat: Add chat-agent switcher in chat ui

fix: qml errors

refactor: Change top bar layout

fix: default value

* fix: update github action for qtc
2025-10-31 16:09:38 +01:00
9117572f82 feat: Add Text slider button 2025-10-31 09:52:22 +01:00
a143cc8e20 fix: Add ctrl+enter shortcut 2025-10-31 09:13:40 +01:00
eae2b748d5 fix: Chat items width 2025-10-31 09:05:17 +01:00
64bca47290 feat: Add support QtCreator 18 (#246) 2025-10-31 04:08:37 +01:00
531fce96b5 fix: Build QString() compilation error 2025-10-29 01:15:50 +01:00
e7e437590a fix: Build error and tool guideline 2025-10-29 01:09:14 +01:00
00b7287e08 refactor: Back use tools by default and disabling auto apply 2025-10-29 01:01:51 +01:00
5a49a2e7eb refactor: Optimize searching tools
refactor: Merge read and find tool
2025-10-29 00:56:53 +01:00
3b56c1f07a feat: Add build tool 2025-10-28 23:50:44 +01:00
d483ca372d revert: Remove edit file tool (#245) 2025-10-28 16:39:49 +01:00
dfd9979450 refactor: Optimize tool guidelines and disable tools by default 2025-10-26 21:04:37 +01:00
6cb0b14b18 feat: Add log to search tool 2025-10-26 11:57:37 +01:00
43b64b9166 refactor: Simplified edit tool (#242)
refactor: Re-work edit file tool
2025-10-26 11:47:16 +01:00
608103b92e feat: Popup to show current project rules (#241)
* feat: Popup to show current project rules
* feat: Add icon to show project rules button
* feat: Add counter for active rules
2025-10-23 21:41:59 +02:00
e1025df21e feat: Add tool for creating file 2025-10-23 16:32:40 +02:00
fad2453dbe fix: Remove using tools from QuickRefactoring feature 2025-10-23 16:16:47 +02:00
5e1530715c fix: QString() issue in linux build 2025-10-23 16:14:28 +02:00
dfac209c23 feat: Add multiply reading to read files tool 2025-10-23 15:47:10 +02:00
cab8718979 refactor: Find symbol tool return only url and line 2025-10-23 15:07:22 +02:00
5dc28fc1ad refactor: Improve tool guidelines 2025-10-23 15:06:15 +02:00
1122332423 fix: Selection for code changes 2025-10-22 09:19:32 +02:00
7e878cdbf8 fix: Add tool exception for logging purpose 2025-10-21 00:51:42 +02:00
c95b20d6d4 fix: Override file edit tool another assistant message 2025-10-21 00:27:47 +02:00
70c610997a feat: Add settings for read files only from current project 2025-10-21 00:21:53 +02:00
8a4bf54fff feat: Improve tools guidelines 2025-10-20 19:25:21 +02:00
db7da29fa4 fix: Missed QString linux compilation error 2025-10-20 18:52:01 +02:00
a0a76f2665 fix: QString compilation 2025-10-20 18:45:23 +02:00
56354e8d87 feat: Add find file tool 2025-10-20 18:38:12 +02:00
b7322be00c fix: Read file by path description 2025-10-20 18:23:18 +02:00
254fac246d feat: Add settings for auto apply changes 2025-10-20 18:10:21 +02:00
0365018834 fix: Change parameter for read file tool 2025-10-20 16:17:45 +02:00
755be518be fix: Empty context for empty file 2025-10-20 13:52:13 +02:00
fe82b48bef feat: Add find symbol tool
* improve other tools for reading context
2025-10-20 12:32:03 +02:00
8a338ecb69 feat: Add file suggestion edit tool and chat UI (#240)
* feat: Add settings for write to system tool access
2025-10-20 11:48:18 +02:00
238ca00227 Update README.md 2025-10-15 13:28:02 +02:00
18fb2b530f chore: Upgrade plugin to 0.7.1 2025-10-14 02:35:52 +02:00
f0d2e42680 feat: Add support QtCreator 17.0.2 (#239)
feat: Add support QtC 17.0.2
2025-10-14 02:19:14 +02:00
ff0f994ec6 feat: Add project-specific rules support 2025-10-14 01:53:44 +02:00
45df27e749 feat: Add tool for reading issues tab 2025-10-13 18:33:17 +02:00
02863003a9 doc: Added tools info to README.md 2025-10-12 13:29:47 +02:00
002b8e01e5 chore: Upgrade plugin to 0.7.0 2025-10-12 12:28:24 +02:00
f54d1185aa feat: Improve context menu for tool results in chat 2025-10-12 12:16:37 +02:00
bcb0c6f761 fix: Copy selected in code block instead all 2025-10-12 12:03:12 +02:00
d285ab6117 fix: Improve tool handler tools execution 2025-10-12 11:18:20 +02:00
5ae6f9e3bf feat: Add searching tool 2025-10-12 04:25:56 +02:00
fb5903e44f fix: Remove unnecessary log 2025-10-12 03:57:40 +02:00
ce66c8e4f7 feat: Add tools permissions (#238) 2025-10-12 03:56:05 +02:00
5f094887e7 refactor: remove navigation panel 2025-10-12 02:33:21 +02:00
69d9af1a97 feat: Add tooling support to google provider (#237) 2025-10-11 19:46:27 +02:00
86b52bf858 feat: Add context menu to input field and text blocks 2025-10-11 18:35:19 +02:00
cac6068ee7 refactor: Fix copy button and add context menu to code block 2025-10-11 18:23:02 +02:00
8d495dd1bf feat: Add navigation panel for messages 2025-10-11 18:02:08 +02:00
906c161729 feat: Add ollama support tooling (#236) 2025-10-11 10:42:31 +02:00
ebd71daf3d fix: Cleanup accumulated text in one request 2025-10-10 16:45:23 +02:00
84770abb20 fix: Remove duplicate enum 2025-10-10 13:17:16 +02:00
b4e8bdf6da fix: Handling request error on provider error 2025-10-10 10:53:06 +02:00
d2b28093a6 feat: Improve showing tools in chat (#235) 2025-10-10 10:03:22 +02:00
bde58fb9aa Update FUNDING.yml 2025-10-02 13:58:05 +02:00
d4b6f8976b Update FUNDING.yml 2025-10-02 13:56:30 +02:00
cd08b5d919 feat: Add OpenAI compatible providers tooling support (#234)
* remove old providers message handler
2025-10-01 17:23:05 +02:00
f6de03f601 feat: Add llama.cpp tooling support 2025-10-01 16:37:44 +02:00
1a08eebe92 feat: Add Mistral AI tooling support 2025-10-01 15:58:45 +02:00
ea4f8b9df9 feat: Freeze commit hash for gh actions (#233) 2025-10-01 15:32:41 +02:00
7f77f7175d feat: Add tooling support for LM Studio 2025-10-01 12:33:12 +02:00
bed42f9098 feat: Add OpenAI tooling support (#232) 2025-10-01 00:58:54 +02:00
10b924d78a Feat: Add Claude tools support to plugin (#231)
* feat: Add settings for handle using tools in chat
* feat: Add Claude tools support
* fix: Add ai ignore to read project files list tool
* fix: Add ai ignore to read project file by name tool
* fix: Add ai ignore to read current opened files tool
2025-09-30 23:19:46 +02:00
8aa37c5c8c refactor: Move Message type enum to separate header 2025-09-30 19:37:46 +02:00
f8b87da2ca refactor: Remove inja submodule 2025-09-30 19:25:34 +02:00
7663bd34af refactor: Move UI controls to own lib 2025-09-29 18:55:51 +02:00
a52c86c6f0 fix: remove Cmake dev variable 2025-09-29 17:54:35 +02:00
ac53296e85 fix: Change behavior of cancel request
*now cancel request cancel all requests
2025-09-28 15:28:01 +02:00
c688cba3dd fix: Change handling shortcuts for handling copy-paste 2025-09-28 15:12:55 +02:00
ff750c271a feat: Add list project files tool 2025-09-23 00:09:56 +02:00
d0f8c1098f feat: Add tools, fabric and executable handler (#230) 2025-09-22 12:36:13 +02:00
5cde6ffac7 feat: Add tool interface and factory 2025-09-17 22:24:31 +02:00
8c6f1e514b fix: Fully qualified for Provider signals and slots 2025-09-17 20:29:53 +02:00
99cd79aac8 refactor: Add request id as type 2025-09-17 20:27:49 +02:00
d2b6c11569 fix: Reset for char renderer 2025-09-17 19:43:20 +02:00
ec1b5bdf5f refactor: Remove non-streaming support (#229) 2025-09-17 19:38:27 +02:00
561661b476 fix: Compatibility problem with nvenc on windows
* change default chat render to software on windows
2025-09-17 10:20:19 +02:00
76309be0a6 Refactor llm providers to use internal http client (#227)
* refactor: Move http client into provider

* refactor: Rework ollama provider for work with internal http client

* refactor: Rework LM Studio provider to work with internal http client

* refactor: Rework Mistral AI to work with internal http client

* fix: Replace url and header to QNetworkRequest

* refactor: Rework Google provider to use internal http client

* refactor: OpenAI compatible providers switch to use internal http client

* fix: Remove m_requestHandler from tests

* refactor: Remove old handleData method

* fix: Remove LLMClientInterfaceTest
2025-09-03 10:56:05 +02:00
5969d530bd chore: Upgrade plugin to 0.6.2 2025-09-01 00:51:03 +02:00
809f1c6614 feat: Add support QtCreator 17.0.1 (#225)
Add support 17.0.1 instead 17.0.0
2025-09-01 00:49:52 +02:00
851e681cf5 feat: Add new http client 2025-08-30 18:34:10 +02:00
f2f3b7cce0 doc: Add hotkey to close chat view 2025-08-20 09:31:27 +02:00
5b7a9b681c doc: Update chat description in README.md 2025-08-19 11:41:20 +02:00
29af277139 doc: Added chat and hotkeys description 2025-08-18 12:29:03 +02:00
a45786bd00 chore: Upgrade plugin to 0.6.1 2025-08-18 12:07:27 +02:00
695b35b510 feature: Add support Qwen3-coder model (#221)
Add support Qwen3-coder model
Rename template for old
2025-08-18 12:01:34 +02:00
5a23ab9c5a fix: Add compatible version in json info 2025-08-18 10:12:57 +02:00
c36dffea93 refactor: Add model output settings instead smartprocessing setting (#220) 2025-08-17 22:01:26 +02:00
4b7eed2779 fix: Change icon for close chat 2025-08-16 23:36:23 +02:00
88c11c4702 fix: Change status bar icon for show chat 2025-08-16 23:21:29 +02:00
543c79161d fix: Clean connection for workaround http2 windows problem (#219) 2025-08-15 10:17:40 +02:00
aa2edf5954 feature: Add popup window for chat
* feature: Add chat view via QQuickView
* feature: Update chat UI
* fix: Disable chat in navigation panel and bottom bar by default
2025-08-15 09:35:34 +02:00
894fec860a fix: Change LMStudio completion endpoint 2025-08-08 16:38:02 +02:00
e4324f8e80 refactor: Remove checking format on CI 2025-08-08 15:23:38 +02:00
6a0198ae9b refactor: Moved execute function to protected 2025-07-20 12:52:02 +02:00
e136d6056a feat: Add basic task flow run and edit (#212) 2025-07-12 23:44:34 +02:00
ff027b12af fix: Using Qt linguist tool in CI (#210)
* fix: Path to qt tools
* fix: Change TS dir variable for compatibility with Qt6.8
2025-07-04 01:11:51 +02:00
0bdf77f38d feat: Add possibility for translation 2025-07-04 00:40:05 +02:00
21814e8809 fix: Change qtc version typo in README.md 2025-06-24 14:22:30 +02:00
d732e2f9aa chore: Upgrade plugin version to 0.6.0 2025-06-18 20:04:36 +02:00
bf6d09a068 fix: Apply part of code suggestion (#203) 2025-06-18 20:00:18 +02:00
c3f2011c29 Add support QtCreator 17.0.0 (#202)
- removed support QtCreator 16.0.1, 16.0.2 instead
2025-06-18 19:51:51 +02:00
af3fdb58ff fix: Add custom endpoint to reset function 2025-05-18 20:06:46 +02:00
637a4d9d4c feat: Add custom providers endpoint (#188) 2025-05-17 09:21:06 +02:00
7e2345773f doc: Add support QtC 16.0.2 to README.md 2025-05-14 19:20:35 +02:00
14a5ddbdd8 chore: Upgrade plugin to 0.5.13 2025-05-14 19:09:16 +02:00
e178b7daa7 feat: Add multi QtCreator versions
* feat: Add multi qtc version
* feat Upgrade plugin to QtC 16.0.2
2025-05-14 16:06:15 +02:00
4b353d5091 doc: Update targets in README.md 2025-05-10 15:09:35 +02:00
f7ba7b95be doc: Fix ignore files api in README.md 2025-05-02 08:58:31 +02:00
6ae95fec45 doc: Added quick refactoring feature description to README.md 2025-05-02 08:31:34 +02:00
dad8ab2bf3 chore: Update plugin to 0.5.12 2025-05-01 15:38:05 +02:00
25a6983de0 refactor: Make connection more async (#182) 2025-05-01 15:35:33 +02:00
4e05abc7d2 feat: Add settings for text format 2025-05-01 00:01:44 +02:00
784529e344 feat: Add chat font settings (#180) 2025-04-30 22:44:59 +02:00
155153a763 fix: Optimize searching unreadable symbols for markdown 2025-04-30 21:23:43 +02:00
9225c0c1a9 fix: Check readable symbols for markdown 2025-04-29 21:55:44 +02:00
43adc95857 feat: Add a floating "copy" button 2025-04-28 09:25:39 +02:00
ee672f2cda refactor: Remove Chat preview scrollbar 2025-04-26 17:29:06 +02:00
a3edb8a577 chore: Upgrade plugin to 0.5.11 2025-04-24 21:46:32 +02:00
407d3b11c0 fix: Change maximum limit of chat tokens 2025-04-24 21:44:49 +02:00
285e739074 refactor: Change base text style render to markdown 2025-04-24 21:38:54 +02:00
f7e748ba7e chore: Upgrade plugin to 0.5.10 2025-04-24 03:24:00 +02:00
acb1306321 fix: Improve detect unclose codeblock 2025-04-24 03:21:19 +02:00
8b38ecc29b feat: Add Chat preview scroll bar 2025-04-24 02:54:21 +02:00
cfb364f033 fix: Correct removing latest item in messages list 2025-04-24 01:32:01 +02:00
2fe6850a06 refactor: Improve textsuggestion working 2025-04-24 01:25:45 +02:00
3e9506ca92 doc: Add description of ignoring files feature to README.md 2025-04-21 09:40:34 +02:00
d24adff0f5 chore: Update plugin version to 0.5.9 2025-04-21 09:18:02 +02:00
447324eb07 feat: Make artifacts name more meaningful (#172) 2025-04-21 09:17:16 +02:00
4ca494cc51 fix: Suggestion symbols count 2025-04-21 09:02:02 +02:00
8a80dbe8f5 fix: Exclude ignore file from attach 2025-04-21 08:37:57 +02:00
2b539bbdeb fix: Remove check ignoring file on open 2025-04-21 08:08:20 +02:00
3f2c146df1 fix: Save codestral api key 2025-04-20 09:57:14 +02:00
9a54f04a0d feat: Add Codestral as separate ai provider (#171) 2025-04-20 09:48:36 +02:00
7a33425d1a feat: Add reset button to clean message list to specific message (#168) 2025-04-18 19:06:17 +02:00
711aa672f2 fix: Increase mac threshold tokens for tokens (#167) 2025-04-18 17:56:48 +02:00
8cb6a2f6d2 fix: Check patterns and remove filewatcher (#166)
* fix: Check patterns and remove filewatcher
* fix: Don't show log for non-existent ignore file
2025-04-18 10:55:46 +02:00
2f9622e23e Update details for Quick Refactor tool in README.md 2025-04-18 08:18:46 +02:00
674b1fecde chore: Upgrade plugin version to 0.5.8 2025-04-17 10:40:10 +02:00
b36d01d2c7 feat: Improve quick refactor dialog (#165) 2025-04-17 10:34:31 +02:00
615175bea8 feat: Add file list for ignoring in request for llm (#163) 2025-04-17 09:12:47 +02:00
7515599acb doc: Add hotkey description for Quick Refactor to README.md 2025-04-14 14:38:12 +02:00
3652d4d5d9 Fix QtCreator version compatibility 2025-04-14 10:54:01 +02:00
75677770b2 Update QtCreator version compatibility README.md 2025-04-14 10:52:45 +02:00
329a1efd5d Update QtCreator version in README.md 2025-04-14 10:51:51 +02:00
27760a3b99 doc: Add example of Quick Refactor to README.md 2025-04-14 01:59:49 +02:00
a93b3cd7f5 chore: Upgrade plugin to QtCreator 16.0.1 (#162) 2025-04-14 01:32:51 +02:00
bacde51d71 feat: Add quick refactor command via context menu (#161)
* feat: Add quick refactor command via context menu
* feat: Add settings for Quick Refactor
2025-04-14 01:01:44 +02:00
418578743a feat: Prepare widget for chat 2025-04-09 18:27:25 +02:00
56e5ef22f1 doc: Remove commercial support from README.md 2025-04-08 08:14:57 +02:00
e90933d713 fix: Add hack for codellama fim models 2025-04-07 18:55:08 +02:00
5b9c67c2d8 doc: Add sharing opened files feature description to README.md 2025-04-04 22:12:36 +02:00
fe84a2a303 chore: Upgrade plugin version to 0.5.6 2025-04-04 18:39:18 +02:00
62de53c306 chore: Update copyrights 2025-04-04 18:01:02 +02:00
7c6a10936c refactor: Simplify update mechanism (#159) 2025-04-04 17:55:36 +02:00
032c9bbbf3 fix: Add input_extra to llama.cpp validator 2025-04-04 17:16:05 +02:00
8906f98038 fix: Rework copyright searching (#158) 2025-04-04 15:29:36 +02:00
5126092449 doc: Minor Update README.md to include a shortcut on Linux (#157)
Added short-cut for Linux KDE Plasma
2025-04-04 14:37:36 +02:00
9d2d70fc63 feat: Add sharing opened files with code completion requests (#156) 2025-04-04 10:38:06 +02:00
ffaf6bd61b feat: Add code completion request progress animation (#153) 2025-04-02 21:00:45 +02:00
79218d8412 refactor: Replace singletone for context manager (#151) 2025-04-01 22:29:45 +02:00
7e6e526ac8 refactor: Removed deprecated api keys fields 2025-03-27 02:12:07 +01:00
80646e2af0 feat: Add additional language for handling to CodeCompletion settings (#150) 2025-03-27 02:07:09 +01:00
5808a892c1 fix: Change expected name in test 2025-03-27 00:41:28 +01:00
d58ff90458 fix: Fixed typo in the use of the project name 2025-03-27 00:34:10 +01:00
7d06ab04dc feat: Add RunQtCreator target 2025-03-27 00:29:23 +01:00
9d40e8ca25 chore: Upgrade plugin to 0.5.5 version 2025-03-20 19:02:11 +01:00
5b16c5403a fix: Add authorization while getting installed models (#142) (#147) 2025-03-20 11:33:38 +01:00
4ddbe0b8b9 feat: Support Ollama authorization via BaseAuth (#145) (#146) 2025-03-20 11:14:50 +01:00
f41e063c02 chore: Upgrade plugin version to 0.5.4 2025-03-17 02:51:31 +01:00
9d7d084448 fix: Wrong template replace to first template (#143) 2025-03-17 02:48:18 +01:00
1ca1ffc629 fix: Remove reading from replay leading to crash (#142) 2025-03-17 01:22:27 +01:00
8419577ae5 fix: Resolve thread-related QNetworkAccessManager issue (#140)
Fixes "QObject: Cannot create children for a parent that is in a different thread" error by creating QNetworkAccessManager in the same thread where it's used, ensuring proper thread affinity for network operations.
2025-03-16 09:47:04 +01:00
91a6a88130 doc: Add default path for installed plugin 2025-03-14 11:35:37 +01:00
be38abc505 chore: upgrade plugin to 0.5.3 (#139) 2025-03-14 10:59:23 +01:00
f2e0afb6b8 fix: Add qml in code handler for processing model answers (#138) 2025-03-14 10:47:30 +01:00
3cf07238fd doc: Update QtC version to 16 2025-03-14 09:32:54 +01:00
b98f85a997 chore: Upgrade plugin to QtCreator 16 (#136) 2025-03-13 16:46:32 +01:00
085659483f doc: Add info about linux compatibility to README.md 2025-03-11 08:34:14 +01:00
8a1fd5438e chore: Add tests for LLMClientInterface (#131) 2025-03-10 21:54:17 +01:00
78f69e82a5 chore: Checkout submodules when building (#133) 2025-03-10 19:17:30 +01:00
3d770f91c7 refactor: Reduce dependency on TextDocument in ContextManager (#128) 2025-03-10 18:06:19 +01:00
c724bace06 refactor: Move document access out of prepareContext() (#129) 2025-03-10 17:54:03 +01:00
719065ebfc refactor: Extract document reading to separate class (#127)
This decouples LLMClientInterface from Qt Creator text editor
implementation and allows to write tests
2025-03-10 17:42:40 +01:00
a218064a4f refactor: Introduce base class for RequestHandler (#125)
This will make it possible to write a mock implementation.
2025-03-10 17:29:45 +01:00
13cd12b00a chore: Add 3rdparty/inja dependency (#126) 2025-03-10 17:28:25 +01:00
ed59be4199 refactor: Extract performance logging to separate class (#124)
This should not be responsibility of LLMClientInterface. Extracting this
class also adds flexibility to silence logging output in tests.
2025-03-10 17:10:01 +01:00
7dd8b3d085 fix: Make build CMakeLists.txt standalone (#123)
Previously it depended on QODEASSIST_QT_CREATOR_VERSION_* flags being
passed to cmake during build process. Making it standalone saves time
for the users.
2025-03-10 17:00:07 +01:00
3839d6896c refactor: Pass LLMClientInterface to QodeAssistClient (#122)
Contructing LLMClientInterface in constructor of QodeAssistClient when
initializing base class severely limits what can be done. In particular,
no members can be referred to, because nothing of the class instance
itself has been initialized at that point of time.
2025-03-10 16:56:27 +01:00
6b86637dcb doc: Add llama.cpp description to README.md 2025-03-10 12:07:39 +01:00
58c3e26e7f refactor: Decouple LLMClientInterface from ProvidersManager (#120)
This will be needed for tests.
2025-03-10 10:40:51 +01:00
98e1047bf1 refactor: Decouple prompt template manager from their users (#115)
This makes it possible to test the user classes
2025-03-10 02:13:10 +01:00
b6f36d61ae fix: Replace ubuntu-latest to ubuntu 22.04 (#119) 2025-03-10 01:33:03 +01:00
f2f453ccc8 chore: Upgrade plugin to 0.5.1 2025-03-10 01:00:33 +01:00
1bcfd749d5 fix: Change OpenAI model parser conditions 2025-03-10 00:47:16 +01:00
e66f467214 feat: Add llama.cpp provider and fim template (#118) 2025-03-09 22:57:33 +01:00
c9a3cdaf25 refactor: Reuse extractFilePathFromRequest() more (#117) 2025-03-08 16:18:44 +01:00
7c483f89cd chore: Replace deprecated FilePath::toString() with toFSPathString() (#116)
This solves a number of deprecation warnings during build.
FilePath::toFSPathString() has been available since couple of years ago.
2025-03-08 16:08:33 +01:00
6c323642e4 refactor: Inject settings into LLMClientInterface (#114)
This reduces reliance on global state and makes it more possible to test
the code.
2025-03-08 15:08:15 +01:00
3a494d5254 chore: Remove duplicate label setting for systemPromptForNonFimModels (#111)
The same call is several lines below.

Note that systemPrompt does not need label due to a similarly named
checkbox placed nearby. Thus users intuitively know what the text box is
for.
2025-03-08 10:40:22 +01:00
44b3b0cc0c refactor: Don't use global state in ContextManager::isSpecifyCompletion (#112)
Using global state makes testing things way harder.
2025-03-08 10:38:52 +01:00
3aae923d43 feat: Improve describe recent changes in system prompt (#113)
Co-authored-by: Petr Mironychev <9195189+Palm1r@users.noreply.github.com>
2025-03-08 08:58:28 +01:00
f94c79a5ff fix: Improve support for code blocks without language (#108)
This makes it possible to represent code blocks in models that emit
their suggestion immediately after the ``` characters.
2025-03-07 15:30:22 +01:00
9a5047618d chore: Silence Qt warnings during tests (#110) 2025-03-07 01:58:09 +01:00
90beebf2ee Revert "refactor: Move all processing logic to CodeHandler::processText()" (#109) 2025-03-07 01:57:13 +01:00
521261e0a3 refactor: Move all processing logic to CodeHandler::processText() (#107)
This will become useful once more processing modes are available
2025-03-06 18:49:28 +01:00
5536de146c chore: Remove dead code in RequestHandler (#106) 2025-03-06 14:19:01 +01:00
81ac3c71fb chore: Add tests for CodeHandler (#105)
* chore: Extract test utils to separate file

This makes it possible to reuse the utils in other test files.

* chore: Add tests for CodeHandler
2025-03-06 13:45:12 +01:00
61ca5c9a1b fix: Properly omit copyright information (#103)
This commit ensures that copyright information is always excluded and
that context is always split into prefix and suffix at correct position.
2025-03-06 13:00:15 +01:00
8a167bf248 chore: Expand DocumentContextReaderTest to cover more conditions (#102)
Tests should have "before" and "after" cases side by side, so that it's
possible to easily verify that full context is extracted correctly.

Also tests should consistently cover the same conditions in all the
different scenarios.
2025-03-06 12:08:57 +01:00
ab97f39ea4 chore: Fix pretty printing for QString (#101)
Previously QString was printed symbol by symbol. E.g.:

reader.readWholeFileAfter(3, 1)
    Which is: { "i", "n", "e", " ", "2", "
", "L", "i", "n", "e", " ", "3" }
2025-03-06 12:08:16 +01:00
0d3493e7f6 feat: Change linux build base to ubuntu 22.04 (#99) 2025-03-06 11:55:04 +01:00
1d062e1fe4 chore: Use zero-based line numbers in tests (#100)
This makes the line index arguments passed to functions match the actual
test data, thus tests are less confusing.
2025-03-06 11:53:27 +01:00
5dceb5cd19 fix: Bring back old behavior of readStrings{After,Before}Cursor setting (#97) 2025-03-05 20:27:16 +01:00
69a8aa80d9 refactor: Make DocumentContextReader::prepareContext() testable (#96) 2025-03-05 20:18:59 +01:00
e218699e64 refactor: Reuse getContext{Before,After}() instead of duplicating logic (#95)
getContextAfter(int lineNumber, int cursorPosition) and
getContextBefore(int lineNumber, int cursorPosition) are currently not
tested. Thus as little logic as possible should live there.
2025-03-05 19:43:51 +01:00
3dc0d910bf fix: Fix off by one errors in getContext{Before,After}() (#94)
This also specifies what exactly getContext*() functions do. Before this
commit linesCount was sometimes interpreted as exclusive of current
line, which was confusing as linesCount + 1 lines were being returned.
2025-03-05 19:32:53 +01:00
f9f2a86cea fix: Correctly pick whole file context (#85)
Currently the current line is duplicated in both "before" and "after"
context. This is due to DocumentContextReader::readWholeFileAfter()
picking the current line part of which has been already included into
the "before" context.
2025-03-05 19:17:51 +01:00
247256d4a4 chore: Add unit tests for DocumentContextReader (#90)
chore: Add unit tests for DocumentContextReader

The tests are based on GTest like some tests in Qt Creator itself, which
makes it easy to run as full Qt Creator does not need to be started.
2025-03-05 15:01:52 +01:00
bcf7b6c226 refactor: Make DocumentContextReader usable outside Qt Creator context (#89)
This makes it possible to write simple unit tests for it without running
full Qt Creator. Not coupling DocumentContextReader to
TextEditor::TextDocument unnecessarily is also a better design in
general.
2025-03-05 01:53:02 +01:00
29a3939c64 chore: Add Github Actions job for checking formatting (#92) 2025-03-05 01:46:17 +01:00
cb3464170e chore: Run clang-format over the codebase (#91)
This commit is a result of the following commands:

clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
2025-03-05 01:45:15 +01:00
ca0fb5efbb fix: Make plugin registration be compatible with Qt Creator 16 (#80)
This introduces changes needed after the following commit in Qt Creator:

ba5e4b7eff

Core: Provide settings categories centrally
2025-03-04 11:29:30 +01:00
d8a01504a3 chore: Add support for Qt Creator version to the plugin build script (#87)
This will allow to add code conditional on the Qt Creator version to the
plugin codebase. The Qt Creator version will be passed from the build
script automatically. This will also allow to easily extend the Github
Actions job matrix to create releases for more than one Qt Creator
version.

Using QT_VERSION_CHECK allows to reuse existing Qt patterns of checking
versions.

Code has been tested by invoking QODEASSIST_QT_CREATOR_VERSION in code.
2025-03-04 11:17:19 +01:00
3b188740e8 fix: Make settings dialogs button order consistent (#84)
Currently on Linux most dialogs follow the following order of action
buttons: "OK", "Cancel" (left to right). However, several dialogs are
constructed explicitly and don't follow this convention.

This commit fixes this discrepancy.

Fixes: https://github.com/Palm1r/QodeAssist/issues/83
2025-03-03 19:12:01 +01:00
0d22e1866e fix: Add tooltip about where log messages can be seen (#86) 2025-03-03 19:00:25 +01:00
61196cae90 chore: Run clang-format over the codebase (#82)
This commit is a result of the following commands:

clang-format-19 --style=file -i $(git ls-files | fgrep .cpp)
clang-format-19 --style=file -i $(git ls-files | fgrep .hpp)
2025-03-02 22:44:20 +01:00
102bb114a1 refactor: Remove duplicate constants from SettingsConstants.hpp (#81)
These are duplicated in QodeAssistConstants.hpp. This prevents inclusion
of both files into any source file anywhere in the project.
2025-03-02 22:25:25 +01:00
e507d7ee17 doc: Add discord link with description 2025-02-27 11:39:57 +01:00
ed55c829af doc: Add Mistral AI and Google AI 2025-02-27 00:20:32 +01:00
d651a246de chore: Upgrade plugin to version 0.5.0 2025-02-26 23:25:19 +01:00
c8e0f3268e fix: End completion position in lsp answer 2025-02-26 23:12:26 +01:00
84025ec843 feat: Separate system prompt for fin and non-fim models 2025-02-26 22:51:38 +01:00
f6fd411b2d feat: Add moving to api key settings page if needed 2025-02-26 21:51:11 +01:00
903eb50e7a fix: Current template description in General Settings 2025-02-26 13:39:49 +01:00
e8f7f031b6 feat: Enabling Position Independent Code 2025-02-26 13:25:29 +01:00
90ae6cd1c0 feat: Add checkbox for disabling chat 2025-02-26 13:18:30 +01:00
2ad0117498 fix: Crash in open General Settings after another settings tab 2025-02-26 11:15:36 +01:00
9d58565de3 feat: Add template description in general settings 2025-02-25 21:58:39 +01:00
8dba9b4baa fix: Saving mistral and google keys to settings 2025-02-25 19:21:44 +01:00
7ba615a72d feat: Add Google AI provider and template 2025-02-25 14:26:56 +01:00
1b06a651f0 fix: Chat item code style 2025-02-24 19:37:29 +01:00
912c3d8c04 feat: Additional marker for user message in chat 2025-02-24 16:54:45 +01:00
e924029ec2 feat: Add filter templates for each provider 2025-02-23 01:41:47 +01:00
d96f44d42c refactor: Rework providers and templates logic 2025-02-22 19:39:28 +01:00
bd25736a55 refactor: Optimize SystemPrompt for code completion 2025-02-16 16:53:03 +01:00
60936f6d84 refactor: Improve code completion message for Instruct models
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-12 02:05:37 +01:00
7d23d0323f refactor: Improve system prompt and message 2025-02-12 01:47:52 +01:00
1fa6a225a4 Add discord invite
Some checks failed
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:Windows-x64 cc:cl cxx:cl environment_script:C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat name:Windows Latest MSVC os:windows-latest platform:windows_x64]) (push) Has been cancelled
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Has been cancelled
Build plugin / update_json (push) Has been cancelled
Build plugin / release (push) Has been cancelled
2025-02-05 02:01:32 +01:00
362 changed files with 34919 additions and 3180 deletions

2
.github/FUNDING.yml vendored
View File

@ -12,4 +12,4 @@ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cl
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # 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']

View File

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

View File

@ -12,16 +12,13 @@ on:
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.8.1
QT_CREATOR_VERSION: 15.0.1
QT_CREATOR_VERSION_INTERNAL: 15.0.1
MACOS_DEPLOYMENT_TARGET: "11.0"
CMAKE_VERSION: "3.29.6"
NINJA_VERSION: "1.12.1"
jobs:
build:
name: ${{ matrix.config.name }}
name: ${{ matrix.config.name }} (Qt ${{ matrix.qt_config.qt_version }}, QtC ${{ matrix.qt_config.qt_creator_version }})
runs-on: ${{ matrix.config.os }}
outputs:
tag: ${{ steps.git.outputs.tag }}
@ -36,8 +33,8 @@ jobs:
environment_script: "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Auxiliary/Build/vcvars64.bat",
}
- {
name: "Ubuntu Latest GCC", artifact: "Linux-x64",
os: ubuntu-latest,
name: "Ubuntu 22.04 GCC", artifact: "Linux-x64",
os: ubuntu-22.04,
platform: linux_x64,
cc: "gcc", cxx: "g++"
}
@ -47,12 +44,22 @@ jobs:
platform: mac_x64,
cc: "clang", cxx: "clang++"
}
qt_config:
- {
qt_version: "6.8.3",
qt_creator_version: "16.0.2"
}
- {
qt_version: "6.9.2",
qt_creator_version: "17.0.2"
}
- {
qt_version: "6.10.0",
qt_creator_version: "18.0.0"
}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
- name: Checkout submodules
id: git
@ -61,16 +68,21 @@ jobs:
if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
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()
- name: Download Ninja and CMake
uses: lukka/get-cmake@latest
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
with:
cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }}
- name: Install system libs
- name: Install dependencies
shell: cmake -P {0}
run: |
if ("${{ runner.os }}" STREQUAL "Linux")
@ -78,7 +90,13 @@ jobs:
COMMAND sudo apt update
)
execute_process(
COMMAND sudo apt install libgl1-mesa-dev
COMMAND sudo apt install
# build dependencies
libgl1-mesa-dev libgtest-dev libgmock-dev
# runtime dependencies for tests (Qt is downloaded outside package manager,
# thus minimal dependencies must be installed explicitly)
libsecret-1-0 libxcb-cursor0 libxcb-icccm4 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-render0
libxcb-shape0 libxcb-shm0 libxcb-sync1 libxcb-xfixes0 libxcb-xkb1 libxkbcommon-x11-0 xvfb
RESULT_VARIABLE result
)
if (NOT result EQUAL 0)
@ -90,14 +108,19 @@ jobs:
id: qt
shell: cmake -P {0}
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}")
if ("${{ runner.os }}" STREQUAL "Windows")
set(url_os "windows_x86")
set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64")
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_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")
endif()
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(url_os "linux_x64")
if (qt_version VERSION_LESS "6.7.0")
@ -106,12 +129,20 @@ jobs:
set(qt_package_arch_suffix "linux_gcc_64")
endif()
set(qt_dir_prefix "${qt_version}/gcc_64")
set(qt_package_suffix "-Linux-RHEL_8_10-GCC-Linux-RHEL_8_10-X86_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")
endif()
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(url_os "mac_x64")
set(qt_package_arch_suffix "clang_64")
set(qt_dir_prefix "${qt_version}/macos")
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
if (qt_version VERSION_LESS "6.9.1")
set(qt_package_suffix "-MacOS-MacOS_14-Clang-MacOS-MacOS_14-X86_64-ARM64")
else()
set(qt_package_suffix "-MacOS-MacOS_15-Clang-MacOS-MacOS_15-X86_64-ARM64")
endif()
endif()
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
@ -134,7 +165,7 @@ jobs:
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
endfunction()
foreach(package qtbase qtdeclarative)
foreach(package qtbase qtdeclarative qttools)
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
@ -168,10 +199,11 @@ jobs:
endif()
- name: Download Qt Creator
uses: qt-creator/install-dev-package@v1.2
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
with:
version: ${{ env.QT_CREATOR_VERSION }}
version: ${{ matrix.qt_config.qt_creator_version }}
unzip-to: 'qtcreator'
platform: ${{ matrix.config.platform }}
- name: Extract Qt Creator
id: qt_creator
@ -217,7 +249,7 @@ jobs:
COMMAND python
-u
"${{ 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 .
--build build
--qt-path "${{ steps.qt.outputs.qt_dir }}"
@ -233,67 +265,24 @@ jobs:
endif()
- name: Upload
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
with:
path: ./${{ env.PLUGIN_NAME }}-${{ env.QT_CREATOR_VERSION }}-${{ matrix.config.artifact }}.7z
name: ${{ 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}}-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: matrix.config.os == 'ubuntu-latest'
uses: actions/upload-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./build/build/${{ env.PLUGIN_NAME }}.json
update_json:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Download the JSON file
uses: actions/download-artifact@v4
with:
name: ${{ env.PLUGIN_NAME }}-origin-json
path: ./${{ env.PLUGIN_NAME }}-origin
- name: Store Release upload_url
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
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
xvfb-run ./build/build/test/QodeAssistTest
release:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
needs: [build, update_json]
runs-on: ubuntu-22.04
needs: [build]
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
with:
path: release-with-dirs
@ -302,9 +291,21 @@ jobs:
mkdir 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
id: create_release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

6
.gitignore vendored
View File

@ -73,4 +73,8 @@ CMakeLists.txt.user*
*.dll
*.exe
/build
/build
/.qodeassist
/.cursor
/.vscode
.qtc_clangd/compile_commands.json

0
.gitmodules vendored Normal file
View File

View File

@ -8,15 +8,41 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
find_package(QtCreator REQUIRED COMPONENTS Core)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network REQUIRED)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Test LinguistTools REQUIRED)
find_package(GTest)
qt_standard_project_setup(I18N_TRANSLATED_LANGUAGES en)
# IDE_VERSION is defined by QtCreator package
string(REGEX MATCH "([0-9]+)\\.([0-9]+)\\.([0-9]+)" version_match ${IDE_VERSION})
set(QODEASSIST_QT_CREATOR_VERSION_MAJOR ${CMAKE_MATCH_1})
set(QODEASSIST_QT_CREATOR_VERSION_MINOR ${CMAKE_MATCH_2})
set(QODEASSIST_QT_CREATOR_VERSION_PATCH ${CMAKE_MATCH_3})
if(NOT version_match)
message(FATAL_ERROR "Failed to parse Qt Creator version string: ${IDE_VERSION}")
endif()
message(STATUS "Qt Creator Version: ${QODEASSIST_QT_CREATOR_VERSION_MAJOR}.${QODEASSIST_QT_CREATOR_VERSION_MINOR}.${QODEASSIST_QT_CREATOR_VERSION_PATCH}")
add_definitions(
-DQODEASSIST_QT_CREATOR_VERSION_MAJOR=${QODEASSIST_QT_CREATOR_VERSION_MAJOR}
-DQODEASSIST_QT_CREATOR_VERSION_MINOR=${QODEASSIST_QT_CREATOR_VERSION_MINOR}
-DQODEASSIST_QT_CREATOR_VERSION_PATCH=${QODEASSIST_QT_CREATOR_VERSION_PATCH}
)
add_subdirectory(llmcore)
add_subdirectory(settings)
add_subdirectory(logger)
add_subdirectory(UIControls)
add_subdirectory(ChatView)
add_subdirectory(context)
if(GTest_FOUND)
add_subdirectory(test)
endif()
add_qtc_plugin(QodeAssist
PLUGIN_DEPENDS
@ -24,6 +50,7 @@ add_qtc_plugin(QodeAssist
QtCreator::LanguageClient
QtCreator::TextEditor
QtCreator::ProjectExplorer
QtCreator::CppEditor
DEPENDS
Qt::Core
Qt::Gui
@ -32,6 +59,7 @@ add_qtc_plugin(QodeAssist
Qt::Network
QtCreator::ExtensionSystem
QtCreator::Utils
QtCreator::CPlusPlus
QodeAssistChatViewplugin
SOURCES
.github/workflows/build_cmake.yml
@ -43,33 +71,99 @@ add_qtc_plugin(QodeAssist
LLMClientInterface.hpp LLMClientInterface.cpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/StarCoder2Fim.hpp
templates/DeepSeekCoderFim.hpp
templates/CustomFimTemplate.hpp
templates/Qwen.hpp
templates/Ollama.hpp
templates/BasicChat.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
# templates/DeepSeekCoderFim.hpp
# templates/CustomFimTemplate.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/MistralAIProvider.hpp providers/MistralAIProvider.cpp
providers/LMStudioProvider.hpp providers/LMStudioProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/ClaudeProvider.hpp providers/ClaudeProvider.cpp
providers/OpenAIProvider.hpp providers/OpenAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
RefactorSuggestion.hpp RefactorSuggestion.cpp
RefactorSuggestionHoverHandler.hpp RefactorSuggestionHoverHandler.cpp
QodeAssistClient.hpp QodeAssistClient.cpp
chat/ChatOutputPane.h chat/ChatOutputPane.cpp
chat/NavigationPanel.hpp chat/NavigationPanel.cpp
ConfigurationManager.hpp ConfigurationManager.cpp
CodeHandler.hpp CodeHandler.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
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsFactory.hpp tools/ToolsFactory.cpp
tools/ReadVisibleFilesTool.hpp tools/ReadVisibleFilesTool.cpp
tools/ToolHandler.hpp tools/ToolHandler.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/ToolsManager.hpp tools/ToolsManager.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/EditFileTool.hpp tools/EditFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
tools/FindAndReadFileTool.hpp tools/FindAndReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
providers/ClaudeMessage.hpp providers/ClaudeMessage.cpp
providers/OpenAIMessage.hpp providers/OpenAIMessage.cpp
providers/OllamaMessage.hpp providers/OllamaMessage.cpp
providers/GoogleMessage.hpp providers/GoogleMessage.cpp
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()
#TODO change to TS_OUTPUT_DIRECTORY after removing Qt6.8
qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete
)

View File

@ -6,17 +6,26 @@ qt_policy(SET QTP0004 NEW)
qt_add_qml_module(QodeAssistChatView
URI ChatView
VERSION 1.0
DEPENDENCIES QtQuick
DEPENDENCIES
QtQuick
QML_FILES
qml/RootItem.qml
qml/ChatItem.qml
qml/Badge.qml
qml/dialog/CodeBlock.qml
qml/dialog/TextBlock.qml
qml/controls/QoAButton.qml
qml/parts/TopBar.qml
qml/parts/BottomBar.qml
qml/parts/AttachedFilesPlace.qml
qml/chatparts/CodeBlock.qml
qml/chatparts/FileEditBlock.qml
qml/chatparts/TextBlock.qml
qml/chatparts/ThinkingBlock.qml
qml/chatparts/ToolBlock.qml
qml/chatparts/ChatItem.qml
qml/controls/AttachedFilesPlace.qml
qml/controls/BottomBar.qml
qml/controls/FileEditsActionBar.qml
qml/controls/RulesViewer.qml
qml/controls/Toast.qml
qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg
@ -24,6 +33,23 @@ qt_add_qml_module(QodeAssistChatView
icons/close-light.svg
icons/link-file-light.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/open-in-editor.svg
icons/apply-changes-button.svg
icons/undo-changes-button.svg
icons/reject-changes-button.svg
icons/thinking-icon-on.svg
icons/thinking-icon-off.svg
SOURCES
ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp
@ -32,6 +58,9 @@ qt_add_qml_module(QodeAssistChatView
MessagePart.hpp
ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp
ChatView.hpp ChatView.cpp
ChatData.hpp
FileItem.hpp FileItem.cpp
)
target_link_libraries(QodeAssistChatView
@ -45,6 +74,8 @@ target_link_libraries(QodeAssistChatView
LLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
QodeAssistLogger
)
target_include_directories(QodeAssistChatView

32
ChatView/ChatData.hpp Normal file
View File

@ -0,0 +1,32 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QObject>
#include <QtQmlIntegration>
namespace QodeAssist::Chat {
Q_NAMESPACE
QML_NAMED_ELEMENT(MessagePartType)
enum class MessagePartType { Code, Text, Image };
Q_ENUM_NS(MessagePartType)
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -18,11 +18,18 @@
*/
#include "ChatModel.hpp"
#include <QtCore/qjsonobject.h>
#include <QtQml>
#include <utils/aspects.h>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#include <QtQml>
#include "ChatAssistantSettings.hpp"
#include "Logger.hpp"
#include "context/ChangesManager.h"
namespace QodeAssist::Chat {
@ -31,10 +38,26 @@ ChatModel::ChatModel(QObject *parent)
{
auto &settings = Settings::chatAssistantSettings();
connect(&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
&ChatModel::tokensThresholdChanged);
&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
@ -61,6 +84,33 @@ QVariant ChatModel::data(const QModelIndex &index, int role) const
}
return filenames;
}
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;
// Generate proper file URL for cross-platform compatibility
if (!m_chatFilePath.isEmpty()) {
QFileInfo fileInfo(m_chatFilePath);
QString baseName = fileInfo.completeBaseName();
QString dirPath = fileInfo.absolutePath();
QString imagesFolder = QDir(dirPath).filePath(baseName + "_images");
QString fullPath = QDir(imagesFolder).filePath(image.storedPath);
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
} else {
imageMap["imageUrl"] = QString();
}
imagesList.append(imageMap);
}
return imagesList;
}
default:
return QVariant();
}
@ -72,6 +122,8 @@ QHash<int, QByteArray> ChatModel::roleNames() const
roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted";
roles[Roles::Images] = "images";
return roles;
}
@ -79,7 +131,8 @@ void ChatModel::addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments)
const QList<Context::ContentFile> &attachments,
const QList<ImageAttachment> &images)
{
QString fullContent = content;
if (!attachments.isEmpty()) {
@ -90,17 +143,59 @@ void ChatModel::addMessage(
}
}
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id) {
if (!m_messages.isEmpty() && !id.isEmpty() && m_messages.last().id == id
&& m_messages.last().role == role) {
Message &lastMessage = m_messages.last();
lastMessage.content = content;
lastMessage.attachments = attachments;
lastMessage.images = images;
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{role, content, id};
newMessage.attachments = attachments;
newMessage.images = images;
m_messages.append(newMessage);
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));
}
}
}
}
}
}
}
@ -127,19 +222,50 @@ QList<MessagePart> ChatModel::processMessageContent(const QString &content) cons
while (blockMatches.hasNext()) {
auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) {
QString textBetween = content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
QString textBetween
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) {
parts.append({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();
}
if (lastIndex < content.length()) {
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);
}
}
@ -160,6 +286,9 @@ QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) con
case ChatRole::Assistant:
role = "assistant";
break;
case ChatRole::Tool:
case ChatRole::FileEdit:
continue;
default:
continue;
}
@ -195,4 +324,255 @@ QString ChatModel::lastMessageId() const
return !m_messages.isEmpty() ? m_messages.last().id : "";
}
void ChatModel::resetModelTo(int index)
{
if (index < 0 || index >= m_messages.size())
return;
if (index < m_messages.size()) {
beginRemoveRows(QModelIndex(), index, m_messages.size() - 1);
m_messages.remove(index, m_messages.size() - index);
endRemoveRows();
}
}
void ChatModel::addToolExecutionStatus(
const QString &requestId, const QString &toolId, const QString &toolName)
{
QString content = toolName;
LOG_MESSAGE(QString("Adding tool execution status: requestId=%1, toolId=%2, toolName=%3")
.arg(requestId, toolId, toolName));
if (!m_messages.isEmpty() && !toolId.isEmpty() && m_messages.last().id == toolId
&& m_messages.last().role == ChatRole::Tool) {
Message &lastMessage = m_messages.last();
lastMessage.content = content;
LOG_MESSAGE(QString("Updated existing tool message at index %1").arg(m_messages.size() - 1));
emit dataChanged(index(m_messages.size() - 1), index(m_messages.size() - 1));
} else {
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message newMessage{ChatRole::Tool, content, toolId};
m_messages.append(newMessage);
endInsertRows();
LOG_MESSAGE(QString("Created new tool message at index %1 with toolId=%2")
.arg(m_messages.size() - 1)
.arg(toolId));
}
}
void ChatModel::updateToolResult(
const QString &requestId, const QString &toolId, const QString &toolName, const QString &result)
{
if (m_messages.isEmpty() || toolId.isEmpty()) {
LOG_MESSAGE(QString("Cannot update tool result: messages empty=%1, toolId empty=%2")
.arg(m_messages.isEmpty())
.arg(toolId.isEmpty()));
return;
}
LOG_MESSAGE(
QString("Updating tool result: requestId=%1, toolId=%2, toolName=%3, result length=%4")
.arg(requestId, toolId, toolName)
.arg(result.length()));
bool toolMessageFound = false;
for (int i = m_messages.size() - 1; i >= 0; --i) {
if (m_messages[i].id == toolId && m_messages[i].role == ChatRole::Tool) {
m_messages[i].content = toolName + "\n" + result;
emit dataChanged(index(i), index(i));
toolMessageFound = true;
LOG_MESSAGE(QString("Updated tool result at index %1").arg(i));
break;
}
}
if (!toolMessageFound) {
LOG_MESSAGE(QString("WARNING: Tool message with requestId=%1 toolId=%2 not found!")
.arg(requestId, toolId));
}
const QString marker = "QODEASSIST_FILE_EDIT:";
if (result.contains(marker)) {
LOG_MESSAGE(QString("File edit marker detected in tool result"));
int markerPos = result.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < result.length()) {
QString jsonStr = result.mid(jsonStart);
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8(), &parseError);
if (parseError.error != QJsonParseError::NoError) {
LOG_MESSAGE(QString("ERROR: Failed to parse file edit JSON at offset %1: %2")
.arg(parseError.offset)
.arg(parseError.errorString()));
} else if (!doc.isObject()) {
LOG_MESSAGE(
QString("ERROR: Parsed JSON is not an object, is array=%1").arg(doc.isArray()));
} else {
QJsonObject editData = doc.object();
QString editId = editData.value("edit_id").toString();
if (editId.isEmpty()) {
editId = QString("edit_%1").arg(QDateTime::currentMSecsSinceEpoch());
}
LOG_MESSAGE(QString("Adding FileEdit message, editId=%1").arg(editId));
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message fileEditMsg;
fileEditMsg.role = ChatRole::FileEdit;
fileEditMsg.content = result;
fileEditMsg.id = editId;
m_messages.append(fileEditMsg);
endInsertRows();
LOG_MESSAGE(QString("Added FileEdit message with editId=%1").arg(editId));
}
}
}
}
void ChatModel::addThinkingBlock(
const QString &requestId, const QString &thinking, const QString &signature)
{
LOG_MESSAGE(QString("Adding thinking block: requestId=%1, thinking length=%2, signature length=%3")
.arg(requestId)
.arg(thinking.length())
.arg(signature.length()));
QString displayContent = thinking;
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = false;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::addRedactedThinkingBlock(const QString &requestId, const QString &signature)
{
LOG_MESSAGE(
QString("Adding redacted thinking block: requestId=%1, signature length=%2")
.arg(requestId)
.arg(signature.length()));
QString displayContent = "[Thinking content redacted by safety systems]";
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = true;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added redacted thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
{
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].id == messageId) {
m_messages[i].content = newContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
break;
}
}
}
void ChatModel::setLoadingFromHistory(bool loading)
{
m_loadingFromHistory = loading;
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
}
bool ChatModel::isLoadingFromHistory() const
{
return m_loadingFromHistory;
}
void ChatModel::onFileEditApplied(const QString &editId)
{
updateFileEditStatus(editId, "applied", "Successfully applied");
}
void ChatModel::onFileEditRejected(const QString &editId)
{
updateFileEditStatus(editId, "rejected", "Rejected by user");
}
void ChatModel::onFileEditArchived(const QString &editId)
{
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
}
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
{
const QString marker = "QODEASSIST_FILE_EDIT:";
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
const QString &content = m_messages[i].content;
if (content.contains(marker)) {
int markerPos = content.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < content.length()) {
QString jsonStr = content.mid(jsonStart);
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject editData = doc.object();
editData["status"] = status;
editData["status_message"] = statusMessage;
QString updatedContent = marker
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
m_messages[i].content = updatedContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
.arg(editId, status));
break;
}
}
}
}
}
}
void ChatModel::setChatFilePath(const QString &filePath)
{
m_chatFilePath = filePath;
}
QString ChatModel::chatFilePath() const
{
return m_chatFilePath;
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -37,18 +37,29 @@ class ChatModel : public QAbstractListModel
QML_ELEMENT
public:
enum ChatRole { System, User, Assistant };
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
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
{
ChatRole role;
QString content;
QString id;
bool isRedacted = false;
QString signature = QString();
QList<Context::ContentFile> attachments;
QList<ImageAttachment> images;
};
explicit ChatModel(QObject *parent = nullptr);
@ -61,7 +72,8 @@ public:
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {});
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {});
Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
@ -73,12 +85,41 @@ public:
QString currentModel() const;
QString lastMessageId() const;
Q_INVOKABLE void resetModelTo(int index);
void addToolExecutionStatus(
const QString &requestId, const QString &toolId, const QString &toolName);
void updateToolResult(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &result);
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:
void tokensThresholdChanged();
void modelReseted();
private slots:
void onFileEditApplied(const QString &editId);
void onFileEditRejected(const QString &editId);
void onFileEditArchived(const QString &editId);
private:
void updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage);
QVector<Message> m_messages;
bool m_loadingFromHistory = false;
QString m_chatFilePath;
};
} // namespace QodeAssist::Chat

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -23,6 +23,7 @@
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include "llmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat {
@ -37,6 +38,28 @@ class ChatRootView : public QQuickItem
Q_PROPERTY(QStringList linkedFiles READ linkedFiles NOTIFY linkedFilesChanged FINAL)
Q_PROPERTY(int inputTokensCount READ inputTokensCount NOTIFY inputTokensCountChanged FINAL)
Q_PROPERTY(QString chatFileName READ chatFileName NOTIFY chatFileNameChanged FINAL)
Q_PROPERTY(QString textFontFamily READ textFontFamily NOTIFY textFamilyChanged FINAL)
Q_PROPERTY(QString codeFontFamily READ codeFontFamily NOTIFY codeFamilyChanged FINAL)
Q_PROPERTY(int codeFontSize READ codeFontSize NOTIFY codeFontSizeChanged FINAL)
Q_PROPERTY(int textFontSize READ textFontSize NOTIFY textFontSizeChanged FINAL)
Q_PROPERTY(int textFormat READ textFormat NOTIFY textFormatChanged FINAL)
Q_PROPERTY(bool isRequestInProgress READ isRequestInProgress NOTIFY isRequestInProgressChanged FINAL)
Q_PROPERTY(QString lastErrorMessage READ lastErrorMessage NOTIFY lastErrorMessageChanged FINAL)
Q_PROPERTY(QString lastInfoMessage READ lastInfoMessage NOTIFY lastInfoMessageChanged FINAL)
Q_PROPERTY(QVariantList activeRules READ activeRules NOTIFY activeRulesChanged FINAL)
Q_PROPERTY(int activeRulesCount READ activeRulesCount NOTIFY activeRulesCountChanged FINAL)
Q_PROPERTY(bool isAgentMode READ isAgentMode WRITE setIsAgentMode NOTIFY isAgentModeChanged FINAL)
Q_PROPERTY(bool isThinkingMode READ isThinkingMode WRITE setIsThinkingMode NOTIFY isThinkingModeChanged FINAL)
Q_PROPERTY(
bool toolsSupportEnabled READ toolsSupportEnabled NOTIFY toolsSupportEnabledChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
QML_ELEMENT
@ -54,17 +77,24 @@ public:
void autosave();
QString getAutosaveFilePath() const;
QString getAutosaveFilePath(const QString &firstMessage, const QStringList &attachments) const;
QStringList attachmentFiles() const;
QStringList linkedFiles() const;
Q_INVOKABLE void showAttachFilesDialog();
Q_INVOKABLE void addFilesToAttachList(const QStringList &filePaths);
Q_INVOKABLE void removeFileFromAttachList(int index);
Q_INVOKABLE void showLinkFilesDialog();
Q_INVOKABLE void addFilesToLinkList(const QStringList &filePaths);
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 setIsSyncOpenFiles(bool state);
Q_INVOKABLE void openChatHistoryFolder();
Q_INVOKABLE void openRulesFolder();
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
@ -76,7 +106,55 @@ public:
void onEditorCreated(Core::IEditor *editor, const Utils::FilePath &filePath);
QString chatFileName() const;
Q_INVOKABLE QString chatFilePath() const;
void setRecentFilePath(const QString &filePath);
bool shouldIgnoreFileForAttach(const Utils::FilePath &filePath);
QString textFontFamily() const;
QString codeFontFamily() const;
int codeFontSize() const;
int textFontSize() const;
int textFormat() const;
bool isRequestInProgress() const;
void setRequestProgressStatus(bool state);
QString lastErrorMessage() const;
QVariantList activeRules() const;
int activeRulesCount() const;
Q_INVOKABLE QString getRuleContent(int index);
Q_INVOKABLE void refreshRules();
bool isAgentMode() const;
void setIsAgentMode(bool newIsAgentMode);
bool isThinkingMode() const;
void setIsThinkingMode(bool newIsThinkingMode);
bool toolsSupportEnabled() const;
Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId);
Q_INVOKABLE void undoFileEdit(const QString &editId);
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const;
int currentMessageRejectedEdits() const;
QString lastInfoMessage() const;
bool isThinkingSupport() const;
public slots:
void sendMessage(const QString &message);
@ -93,12 +171,37 @@ signals:
void inputTokensCountChanged();
void isSyncOpenFilesChanged();
void chatFileNameChanged();
void textFamilyChanged();
void codeFamilyChanged();
void codeFontSizeChanged();
void textFontSizeChanged();
void textFormatChanged();
void chatRequestStarted();
void isRequestInProgressChanged();
void lastErrorMessageChanged();
void lastInfoMessageChanged();
void activeRulesChanged();
void activeRulesCountChanged();
void isAgentModeChanged();
void isThinkingModeChanged();
void toolsSupportEnabledChanged();
void currentMessageEditsStatsChanged();
void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
private:
void updateFileEditStatus(const QString &editId, const QString &status);
QString getChatsHistoryDir() const;
QString getSuggestedFileName() const;
QString generateChatFileName(const QString &shortMessage, const QString &dir) const;
bool hasImageAttachments(const QStringList &attachments) const;
ChatModel *m_chatModel;
LLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface;
QString m_currentTemplate;
QString m_recentFilePath;
@ -108,6 +211,21 @@ private:
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
bool m_isAgentMode;
bool m_isThinkingMode;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage;
QStringList m_availableConfigurations;
QString m_currentConfiguration;
};
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -20,10 +20,13 @@
#include "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
namespace QodeAssist::Chat {
@ -35,12 +38,20 @@ SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QSt
return {false, "Failed to create directory structure"};
}
QString imagesFolder = getChatImagesFolder(filePath);
QDir dir;
if (!dir.exists(imagesFolder)) {
if (!dir.mkpath(imagesFolder)) {
LOG_MESSAGE(QString("Warning: Failed to create images folder: %1").arg(imagesFolder));
}
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model);
QJsonObject root = serializeChat(model, filePath);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
@ -70,36 +81,68 @@ SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString
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 {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message)
QJsonObject ChatSerializer::serializeMessage(const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
messageObj["isRedacted"] = message.isRedacted;
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
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;
}
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json)
ChatModel::Message ChatSerializer::deserializeMessage(const QJsonObject &json, const QString &chatFilePath)
{
ChatModel::Message message;
message.role = static_cast<ChatModel::ChatRole>(json["role"].toInt());
message.content = json["content"].toString();
message.id = json["id"].toString();
message.isRedacted = json["isRedacted"].toBool(false);
message.signature = json["signature"].toString();
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;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message));
messagesArray.append(serializeMessage(message, chatFilePath));
}
QJsonObject root;
@ -109,20 +152,26 @@ QJsonObject ChatSerializer::serializeChat(const ChatModel *model)
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();
QVector<ChatModel::Message> messages;
messages.reserve(messagesArray.size());
for (const auto &messageValue : messagesArray) {
messages.append(deserializeMessage(messageValue.toObject()));
messages.append(deserializeMessage(messageValue.toObject(), chatFilePath));
}
model->clear();
model->setLoadingFromHistory(true);
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);
LOG_MESSAGE(QString("Loaded message with %1 image(s)").arg(message.images.size()));
}
model->setLoadingFromHistory(false);
return true;
}
@ -139,4 +188,73 @@ bool ChatSerializer::validateVersion(const QString &version)
return version == VERSION;
}
QString ChatSerializer::getChatImagesFolder(const QString &chatFilePath)
{
QFileInfo fileInfo(chatFilePath);
QString baseName = fileInfo.completeBaseName();
QString dirPath = fileInfo.absolutePath();
return QDir(dirPath).filePath(baseName + "_images");
}
bool ChatSerializer::saveImageToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
{
QString imagesFolder = getChatImagesFolder(chatFilePath);
QDir dir;
if (!dir.exists(imagesFolder)) {
if (!dir.mkpath(imagesFolder)) {
LOG_MESSAGE(QString("Failed to create images folder: %1").arg(imagesFolder));
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(imagesFolder).filePath(uniqueName);
QByteArray imageData = 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(imageData) == -1) {
LOG_MESSAGE(QString("Failed to write image data: %1").arg(file.errorString()));
return false;
}
file.close();
storedPath = uniqueName;
LOG_MESSAGE(QString("Saved image: %1 to %2").arg(fileName, fullPath));
return true;
}
QString ChatSerializer::loadImageFromStorage(const QString &chatFilePath, const QString &storedPath)
{
QString imagesFolder = getChatImagesFolder(chatFilePath);
QString fullPath = QDir(imagesFolder).filePath(storedPath);
QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open image file: %1").arg(fullPath));
return QString();
}
QByteArray imageData = file.readAll();
file.close();
return imageData.toBase64();
}
} // namespace QodeAssist::Chat

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -40,10 +40,18 @@ public:
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes
static QJsonObject serializeMessage(const ChatModel::Message &message);
static ChatModel::Message deserializeMessage(const QJsonObject &json);
static QJsonObject serializeChat(const ChatModel *model);
static bool deserializeChat(ChatModel *model, const QJsonObject &json);
static QJsonObject serializeMessage(const ChatModel::Message &message, const QString &chatFilePath);
static ChatModel::Message deserializeMessage(const QJsonObject &json, const QString &chatFilePath);
static QJsonObject serializeChat(const ChatModel *model, const QString &chatFilePath);
static bool deserializeChat(ChatModel *model, const QJsonObject &json, const QString &chatFilePath);
// Image management
static QString getChatImagesFolder(const QString &chatFilePath);
static bool saveImageToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath);
static QString loadImageFromStorage(const QString &chatFilePath, const QString &storedPath);
private:
static const QString VERSION;

View File

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

View File

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

106
ChatView/ChatView.cpp Normal file
View File

@ -0,0 +1,106 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "ChatView.hpp"
#include <QQmlContext>
#include <QQmlEngine>
#include <QSettings>
#include <QVariantMap>
#include <coreplugin/actionmanager/actionmanager.h>
#include <logger/Logger.hpp>
namespace {
constexpr Qt::WindowFlags baseFlags = Qt::Window | Qt::WindowTitleHint | Qt::WindowSystemMenuHint
| Qt::WindowMinimizeButtonHint | Qt::WindowMaximizeButtonHint
| Qt::WindowCloseButtonHint;
}
namespace QodeAssist::Chat {
ChatView::ChatView()
: m_isPin(false)
{
setTitle("QodeAssist Chat");
engine()->rootContext()->setContextProperty("_chatview", this);
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
setResizeMode(QQuickView::SizeRootObjectToView);
setMinimumSize({400, 300});
setFlags(baseFlags);
if (auto action = Core::ActionManager::command("QodeAssist.CloseChatView")) {
m_closeShortcut = new QShortcut(action->keySequence(), this);
connect(m_closeShortcut, &QShortcut::activated, this, &QQuickView::close);
connect(action, &Core::Command::keySequenceChanged, this, [action, this]() {
if (m_closeShortcut) {
m_closeShortcut->setKey(action->keySequence());
}
});
}
restoreSettings();
}
void ChatView::closeEvent(QCloseEvent *event)
{
saveSettings();
event->accept();
}
void ChatView::saveSettings()
{
QSettings settings;
settings.setValue("QodeAssist/ChatView/geometry", geometry());
settings.setValue("QodeAssist/ChatView/pinned", m_isPin);
}
void ChatView::restoreSettings()
{
QSettings settings;
const QRect savedGeometry
= settings.value("QodeAssist/ChatView/geometry", QRect(100, 100, 800, 600)).toRect();
setGeometry(savedGeometry);
const bool pinned = settings.value("QodeAssist/ChatView/pinned", false).toBool();
setIsPin(pinned);
}
bool ChatView::isPin() const
{
return m_isPin;
}
void ChatView::setIsPin(bool newIsPin)
{
if (m_isPin == newIsPin)
return;
m_isPin = newIsPin;
if (m_isPin) {
setFlags(baseFlags | Qt::WindowStaysOnTopHint);
} else {
setFlags(baseFlags);
}
emit isPinChanged();
}
} // namespace QodeAssist::Chat

51
ChatView/ChatView.hpp Normal file
View File

@ -0,0 +1,51 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QQuickView>
#include <QShortcut>
namespace QodeAssist::Chat {
class ChatView : public QQuickView
{
Q_OBJECT
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
public:
ChatView();
bool isPin() const;
void setIsPin(bool newIsPin);
signals:
void isPinChanged();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void saveSettings();
void restoreSettings();
bool m_isPin;
QShortcut *m_closeShortcut;
};
} // namespace QodeAssist::Chat

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,59 +19,99 @@
#include "ClientInterface.hpp"
#include <texteditor/textdocument.h>
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QUuid>
#include <texteditor/textdocument.h>
#include <coreplugin/editormanager/editormanager.h>
#include <coreplugin/editormanager/ieditor.h>
#include <coreplugin/idocument.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectexplorer.h>
#include <projectexplorer/projectmanager.h>
#include <texteditor/textdocument.h>
#include <texteditor/texteditor.h>
#include "ChatAssistantSettings.hpp"
#include "ContextManager.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "ToolsSettings.hpp"
#include "Logger.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "RequestConfig.hpp"
#include <context/ChangesManager.h>
#include <RulesLoader.hpp>
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(ChatModel *chatModel, QObject *parent)
ClientInterface::ClientInterface(
ChatModel *chatModel, LLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent)
, m_requestHandler(new LLMCore::RequestHandler(this))
, m_chatModel(chatModel)
{
connect(m_requestHandler,
&LLMCore::RequestHandler::completionReceived,
this,
[this](const QString &completion, const QJsonObject &request, bool isComplete) {
handleLLMResponse(completion, request, isComplete);
});
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{}
connect(m_requestHandler,
&LLMCore::RequestHandler::requestFinished,
this,
[this](const QString &, bool success, const QString &errorString) {
if (!success) {
emit errorOccurred(errorString);
}
});
}
ClientInterface::~ClientInterface() = default;
void ClientInterface::sendMessage(
const QString &message, const QList<QString> &attachments, const QList<QString> &linkedFiles)
ClientInterface::~ClientInterface()
{
cancelRequest();
}
auto attachFiles = Context::ContextManager::instance().getContentFiles(attachments);
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", attachFiles);
void ClientInterface::sendMessage(
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useAgentMode)
{
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);
}
}
auto attachFiles = m_contextManager->getContentFiles(textFiles);
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::saveImageToStorage(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, "", attachFiles, imageAttachments);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
@ -84,8 +124,7 @@ void ClientInterface::sendMessage(
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = LLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
@ -93,52 +132,149 @@ void ClientInterface::sendMessage(
}
LLMCore::ContextData context;
context.prefix = message;
context.suffix = "";
QString systemPrompt;
if (chatAssistantSettings.useSystemPrompt())
systemPrompt = chatAssistantSettings.systemPrompt();
const bool isToolsEnabled = Settings::toolsSettings().useTools() && useAgentMode;
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
systemPrompt += QString("\n# Active Project path: %1").arg(project->projectDirectory().toUrlishString());
QString projectRules
= LLMCore::RulesLoader::loadRulesForProject(project, LLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
}
QJsonObject providerRequest;
providerRequest["model"] = Settings::generalSettings().caModel();
providerRequest["stream"] = chatAssistantSettings.stream();
providerRequest["messages"] = m_chatModel->prepareMessagesForRequest(systemPrompt);
if (promptTemplate)
promptTemplate->prepareRequest(providerRequest, context);
else
qWarning("No prompt template found");
if (provider)
provider->prepareRequest(providerRequest, LLMCore::RequestType::Chat);
else
qWarning("No provider found");
QVector<LLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
continue;
}
LLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->supportImage() && !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
}
if (!imageFiles.isEmpty() && !provider->supportImage()) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::Chat;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest = providerRequest;
config.multiLineCompletion = false;
config.apiKey = provider->apiKey();
QJsonObject request;
request["id"] = QUuid::createUuid().toString();
auto errors = config.provider->validateRequest(config.providerRequest, promptTemplate->type());
if (!errors.isEmpty()) {
LOG_MESSAGE("Validate errors for chat request:");
LOG_MESSAGES(errors);
return;
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().caUrl(),
Settings::generalSettings().caModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().caUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().caModel()}, {"stream", true}};
}
m_requestHandler->sendLLMRequest(config, request);
config.apiKey = provider->apiKey();
config.provider->prepareRequest(
config.providerRequest,
promptTemplate,
context,
LLMCore::RequestType::Chat,
isToolsEnabled,
Settings::chatAssistantSettings().enableThinkingMode());
QString requestId = QUuid::createUuid().toString();
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId);
connect(
provider,
&LLMCore::Provider::partialResponseReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionStarted,
m_chatModel,
&ChatModel::addToolExecutionStatus,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::toolExecutionCompleted,
m_chatModel,
&ChatModel::updateToolResult,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::continuationStarted,
this,
&ClientInterface::handleCleanAccumulatedData,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::thinkingBlockReceived,
m_chatModel,
&ChatModel::addThinkingBlock,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::redactedThinkingBlockReceived,
m_chatModel,
&ChatModel::addRedactedThinkingBlock,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
}
void ClientInterface::clearMessages()
@ -149,25 +285,37 @@ void ClientInterface::clearMessages()
void ClientInterface::cancelRequest()
{
auto id = m_chatModel->lastMessageId();
m_requestHandler->cancelRequest(id);
QSet<LLMCore::Provider *> providers;
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, nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
m_accumulatedResponses.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void ClientInterface::handleLLMResponse(const QString &response,
const QJsonObject &request,
bool isComplete)
void ClientInterface::handleLLMResponse(const QString &response, const QJsonObject &request)
{
const auto message = response.trimmed();
if (!message.isEmpty()) {
QString messageId = request["id"].toString();
m_chatModel->addMessage(message, ChatModel::ChatRole::Assistant, messageId);
if (isComplete) {
LOG_MESSAGE(
"Message completed. Final response for message " + messageId + ": " + response);
emit messageReceivedCompletely();
}
}
}
@ -186,30 +334,185 @@ QString ClientInterface::getCurrentFileContext() const
}
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toString());
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
QString content = textDocument->document()->toPlainText();
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toString()));
LOG_MESSAGE(QString("Got context from file: %1").arg(textDocument->filePath().toFSPathString()));
return QString("Current file context:\n%1\nFile content:\n%2").arg(fileInfo, content);
}
QString ClientInterface::getSystemPromptWithLinkedFiles(const QString &basePrompt, const QList<QString> &linkedFiles) const
QString ClientInterface::getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const
{
QString updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = Context::ContextManager::instance().getContentFiles(linkedFiles);
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n")
.arg(file.filename, file.content);
updatedPrompt += QString("\nFile: %1\nContent:\n%2\n").arg(file.filename, file.content);
}
}
return updatedPrompt;
}
Context::ContextManager *ClientInterface::contextManager() const
{
return m_contextManager;
}
void ClientInterface::handlePartialResponse(const QString &requestId, const QString &partialText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError;
bool applySuccess = Context::ChangesManager::instance()
.applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
m_accumulatedResponses.remove(requestId);
}
}
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error);
if (it != m_activeRequests.end()) {
m_activeRequests.erase(it);
}
if (m_accumulatedResponses.contains(requestId)) {
m_accumulatedResponses.remove(requestId);
}
}
void ClientInterface::handleCleanAccumulatedData(const QString &requestId)
{
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
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<LLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<LLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data = ChatSerializer::loadImageFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
LLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath)
{
m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath);
}
QString ClientInterface::chatFilePath() const
{
return m_chatFilePath;
}
} // namespace QodeAssist::Chat

View File

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

76
ChatView/FileItem.cpp Normal file
View File

@ -0,0 +1,76 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "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

48
ChatView/FileItem.hpp Normal file
View File

@ -0,0 +1,48 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <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;
};
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,33 +19,28 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <QObject>
#include <QtQmlIntegration>
#include "ChatData.hpp"
namespace QodeAssist::Chat {
Q_NAMESPACE
class MessagePart
{
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 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)
public:
enum PartType { Code, Text };
Q_ENUM(PartType)
PartType type;
MessagePartType type;
QString text;
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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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"/>
<path d="M6 35L38 6" stroke="black" stroke-width="4" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View 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

View 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

View 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

View 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

View File

@ -1,137 +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
height: msgColumn.implicitHeight + 10
radius: 8
ColumnLayout {
id: msgColumn
width: parent.width
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
}
}
}
}
}
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
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -22,8 +22,10 @@ import QtQuick.Controls
import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts
import ChatView
import "./controls"
import "./parts"
import UIControls
import Qt.labs.platform as Platform
import "./chatparts"
ChatRootView {
id: root
@ -56,6 +58,24 @@ ChatRootView {
color: palette.window
}
SplitDropZone {
anchors.fill: parent
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)
}
}
}
ColumnLayout {
anchors.fill: parent
spacing: 0
@ -64,40 +84,93 @@ ChatRootView {
id: topBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
Layout.preferredHeight: childrenRect.height + 10
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
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 {
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()
rulesButton.onClicked: rulesViewer.open()
activeRulesCount: root.activeRulesCount
pinButton {
visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
agentModeSwitch {
checked: root.isAgentMode
enabled: root.toolsSupportEnabled
onToggled: {
root.isAgentMode = agentModeSwitch.checked
}
}
thinkingMode {
checked: root.isThinkingMode
enabled: root.isThinkingSupport
onCheckedChanged: {
root.isThinkingMode = thinkingMode.checked
}
}
configSelector {
model: root.availableConfigurations
displayText: root.currentConfiguration
onActivated: function(index) {
if (index > 0) {
root.applyConfiguration(root.availableConfigurations[index])
}
}
popup.onAboutToShow: {
root.loadAvailableConfigurations()
}
}
}
ListView {
id: chatListView
signal hideServiceComponents(int itemIndex)
Layout.fillWidth: true
Layout.fillHeight: true
leftMargin: 5
model: root.chatModel
clip: true
spacing: 10
spacing: 0
boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000
delegate: ChatItem {
delegate: Loader {
id: componentLoader
required property var model
required property int index
width: ListView.view.width - scroll.width
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
color: model.roleType === ChatModel.User ? palette.alternateBase
: palette.base
sourceComponent: {
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
}
}
onLoaded: {
if (componentLoader.sourceComponent == chatItemComponent) {
chatListView.hideServiceComponents(index)
}
}
}
header: Item {
@ -118,6 +191,106 @@ ChatRootView {
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)
}
}
}
Component {
id: toolMessageComponent
ToolBlock {
id: toolsItem
width: parent.width
toolContent: model.content
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
toolsItem.headerOpacity = 0.5
}
}
}
}
}
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 {
id: thinking
width: parent.width
thinkingContent: {
let content = model.content
let signatureStart = content.indexOf("\n[Signature:")
if (signatureStart >= 0) {
return content.substring(0, signatureStart)
}
return content
}
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
Connections {
target: chatListView
function onHideServiceComponents(itemIndex) {
if (index !== itemIndex) {
thinking.headerOpacity = 0.5
}
}
}
}
}
}
ScrollView {
@ -130,7 +303,9 @@ ChatRootView {
QQC.TextArea {
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
color: palette.text
background: Rectangle {
@ -153,15 +328,53 @@ ChatRootView {
onTextChanged: root.calculateMessageTokensCount(messageInput.text)
Keys.onPressed: function(event) {
if ((event.key === Qt.Key_Return || event.key === Qt.Key_Enter) && !(event.modifiers & Qt.ShiftModifier)) {
root.sendChatMessage()
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 {
id: attachedFilesPlace
@ -184,23 +397,53 @@ ChatRootView {
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 {
id: bottomBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: 40
sendButton.onClicked: root.sendChatMessage()
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.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
syncOpenFiles {
checked: root.isSyncOpenFiles
onCheckedChanged: root.setIsSyncOpenFiles(bottomBar.syncOpenFiles.checked)
}
attachFiles.onClicked: root.showAttachFilesDialog()
attachImages.onClicked: root.showAddImageDialog()
linkFiles.onClicked: root.showLinkFilesDialog()
}
}
Shortcut {
id: sendMessageShortcut
sequences: ["Ctrl+Return", "Ctrl+Enter"]
context: Qt.WindowShortcut
onActivated: {
if (messageInput.activeFocus && !Qt.inputMethod.visible) {
root.sendChatMessage()
}
}
}
function clearChat() {
root.chatModel.clear()
root.clearAttachmentFiles()
@ -216,4 +459,55 @@ ChatRootView {
messageInput.text = ""
scrollToBottom()
}
Toast {
id: errorToast
z: 1000
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
Toast {
id: infoToast
z: 1000
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
RulesViewer {
id: rulesViewer
width: parent.width * 0.8
height: parent.height * 0.8
x: (parent.width - width) / 2
y: (parent.height - height) / 2
activeRules: root.activeRules
ruleContentAreaText: root.getRuleContent(rulesViewer.rulesCurrentIndex)
onRefreshRules: root.refreshRules()
onOpenRulesFolder: root.openRulesFolder()
}
Connections {
target: root
function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage)
}
}
function onLastInfoMessageChanged() {
if (root.lastInfoMessage.length > 0) {
infoToast.show(root.lastInfoMessage)
}
}
}
Component.onCompleted: {
messageInput.forceActiveFocus()
}
}

View File

@ -0,0 +1,301 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
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)
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: 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
}
}
}
}
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)
}
ToolTip.visible: hovered
ToolTip.text: qsTr("Reset chat to this message and edit")
ToolTip.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
}
}
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 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: palette.base
border.width: 1
border.color: palette.mid
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
}
}
}
}

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
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
}
}
]
}

View File

@ -0,0 +1,469 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import UIControls
import ChatView
import Qt.labs.platform as Platform
Rectangle {
id: root
property string editContent: ""
readonly property var editData: parseEditData(editContent)
readonly property string filePath: editData.file || ""
readonly property string fileName: getFileName(filePath)
readonly property string editStatus: editData.status || "pending"
readonly property string statusMessage: editData.status_message || ""
readonly property string oldContent: editData.old_content || ""
readonly property string newContent: editData.new_content || ""
signal applyEdit(string editId)
signal rejectEdit(string editId)
signal undoEdit(string editId)
signal openInEditor(string editId)
readonly property int borderRadius: 4
readonly property int contentMargin: 10
readonly property int contentBottomPadding: 20
readonly property int headerPadding: 8
readonly property int statusIndicatorWidth: 4
readonly property bool isPending: editStatus === "pending"
readonly property bool isApplied: editStatus === "applied"
readonly property bool isRejected: editStatus === "rejected"
readonly property bool isArchived: editStatus === "archived"
readonly property color appliedColor: Qt.rgba(0.2, 0.8, 0.2, 0.8)
readonly property color revertedColor: Qt.rgba(0.8, 0.6, 0.2, 0.8)
readonly property color rejectedColor: Qt.rgba(0.8, 0.2, 0.2, 0.8)
readonly property color archivedColor: Qt.rgba(0.5, 0.5, 0.5, 0.8)
readonly property color pendingColor: palette.highlight
readonly property color appliedBgColor: Qt.rgba(0.2, 0.8, 0.2, 0.3)
readonly property color revertedBgColor: Qt.rgba(0.8, 0.6, 0.2, 0.3)
readonly property color rejectedBgColor: Qt.rgba(0.8, 0.2, 0.2, 0.3)
readonly property color archivedBgColor: Qt.rgba(0.5, 0.5, 0.5, 0.3)
readonly property string codeFontFamily: {
switch (Qt.platform.os) {
case "windows": return "Consolas"
case "osx": return "Menlo"
case "linux": return "DejaVu Sans Mono"
default: return "monospace"
}
}
readonly property int codeFontSize: Qt.application.font.pointSize
readonly property color statusColor: {
if (isArchived) return archivedColor
if (isApplied) return appliedColor
if (isRejected) return rejectedColor
return pendingColor
}
readonly property color statusBgColor: {
if (isArchived) return archivedBgColor
if (isApplied) return appliedBgColor
if (isRejected) return rejectedBgColor
return palette.button
}
readonly property string statusText: {
if (isArchived) return qsTr("ARCHIVED")
if (isApplied) return qsTr("APPLIED")
if (isRejected) return qsTr("REJECTED")
return qsTr("PENDING")
}
readonly property int addedLines: countLines(newContent)
readonly property int removedLines: countLines(oldContent)
function parseEditData(content) {
try {
const marker = "QODEASSIST_FILE_EDIT:";
let jsonStr = content;
if (content.indexOf(marker) >= 0) {
jsonStr = content.substring(content.indexOf(marker) + marker.length);
}
return JSON.parse(jsonStr);
} catch (e) {
return {
edit_id: "",
file: "",
old_content: "",
new_content: "",
status: "error",
status_message: ""
};
}
}
function getFileName(path) {
if (!path) return "";
const parts = path.split('/');
return parts[parts.length - 1];
}
function countLines(text) {
if (!text) return 0;
return text.split('\n').length;
}
implicitHeight: fileEditView.implicitHeight
ChatUtils {
id: utils
}
Rectangle {
id: fileEditView
property bool expanded: false
anchors.fill: parent
implicitHeight: expanded ? headerArea.height + contentColumn.implicitHeight + root.contentBottomPadding + root.contentMargin * 2
: headerArea.height
radius: root.borderRadius
color: palette.base
border.width: 1
border.color: root.isPending
? (color.hslLightness > 0.5 ? Qt.darker(color, 1.3) : Qt.lighter(color, 1.3))
: Qt.alpha(root.statusColor, 0.6)
clip: true
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
states: [
State {
name: "expanded"
when: fileEditView.expanded
PropertyChanges { target: contentColumn; opacity: 1 }
},
State {
name: "collapsed"
when: !fileEditView.expanded
PropertyChanges { target: contentColumn; opacity: 0 }
}
]
transitions: Transition {
NumberAnimation {
properties: "opacity"
duration: 200
easing.type: Easing.InOutQuad
}
}
MouseArea {
id: headerArea
width: parent.width
height: headerRow.height + 16
cursorShape: Qt.PointingHandCursor
onClicked: fileEditView.expanded = !fileEditView.expanded
RowLayout {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
right: actionButtons.left
leftMargin: root.contentMargin
rightMargin: root.contentMargin
}
spacing: root.headerPadding
Rectangle {
width: root.statusIndicatorWidth
height: headerText.height
radius: 2
color: root.statusColor
}
Text {
id: headerText
Layout.fillWidth: true
text: {
var modeText = root.oldContent.length > 0 ? qsTr("Replace") : qsTr("Append")
if (root.oldContent.length > 0) {
return qsTr("%1: %2 (+%3 -%4)")
.arg(modeText)
.arg(root.fileName)
.arg(root.addedLines)
.arg(root.removedLines)
} else {
return qsTr("%1: %2 (+%3)")
.arg(modeText)
.arg(root.fileName)
.arg(root.addedLines)
}
}
font.pixelSize: 12
font.bold: true
color: palette.text
elide: Text.ElideMiddle
}
Text {
text: fileEditView.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
Rectangle {
visible: !root.isPending
Layout.preferredWidth: badgeText.width + 12
Layout.preferredHeight: badgeText.height + 4
color: root.statusBgColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: root.statusText
font.pixelSize: 9
font.bold: true
color: root.isArchived ? Qt.rgba(0.6, 0.6, 0.6, 1.0) : palette.text
}
}
}
Row {
id: actionButtons
anchors {
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 6
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/open-in-editor.svg"
height: 15
width: 15
}
hoverEnabled: true
onClicked: root.openInEditor(editData.edit_id)
ToolTip.visible: hovered
ToolTip.text: qsTr("Open file in editor and navigate to changes")
ToolTip.delay: 500
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/apply-changes-button.svg"
height: 15
width: 15
} enabled: (root.isPending || root.isRejected) && !root.isArchived
visible: !root.isApplied && !root.isArchived
onClicked: root.applyEdit(editData.edit_id)
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
height: 15
width: 15
}
enabled: root.isApplied && !root.isArchived
visible: root.isApplied && !root.isArchived
onClicked: root.undoEdit(editData.edit_id)
}
QoAButton {
icon {
source: "qrc:/qt/qml/ChatView/icons/reject-changes-button.svg"
height: 15
width: 15
}
enabled: root.isPending && !root.isArchived
visible: root.isPending && !root.isArchived
onClicked: root.rejectEdit(editData.edit_id)
}
}
}
ColumnLayout {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: headerArea.bottom
margins: root.contentMargin
}
spacing: 8
visible: opacity > 0
Text {
Layout.fillWidth: true
text: root.filePath
font.pixelSize: 10
color: palette.mid
elide: Text.ElideMiddle
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: oldContentColumn.implicitHeight + 12
color: Qt.rgba(1, 0.2, 0.2, 0.1)
radius: 4
border.width: 1
border.color: Qt.rgba(1, 0.2, 0.2, 0.3)
visible: root.oldContent.length > 0
Column {
id: oldContentColumn
width: parent.width
x: 6
y: 6
spacing: 4
TextEdit {
id: oldContentText
width: parent.width - 12
height: contentHeight
text: root.oldContent
font.family: root.codeFontFamily
font.pixelSize: root.codeFontSize
color: palette.text
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectByKeyboard: true
textFormat: TextEdit.PlainText
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: oldConentContextMenu.open()
}
}
Platform.Menu {
id: oldConentContextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = oldContentText.selectedText || root.oldContent
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: fileEditView.expanded = !fileEditView.expanded
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: newContentColumn.implicitHeight + 12
color: Qt.rgba(0.2, 0.8, 0.2, 0.1)
radius: 4
border.width: 1
border.color: Qt.rgba(0.2, 0.8, 0.2, 0.3)
Column {
id: newContentColumn
width: parent.width
x: 6
y: 6
spacing: 4
TextEdit {
id: newContentText
width: parent.width - 12
height: contentHeight
text: root.newContent
font.family: root.codeFontFamily
font.pixelSize: root.codeFontSize
color: palette.text
wrapMode: TextEdit.Wrap
readOnly: true
selectByMouse: true
selectByKeyboard: true
textFormat: TextEdit.PlainText
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: newContentContextMenu.open()
}
}
Platform.Menu {
id: newContentContextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = newContentText.selectedText || root.newContent
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: fileEditView.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: fileEditView.expanded = !fileEditView.expanded
}
}
}
}
Text {
Layout.fillWidth: true
visible: root.statusMessage.length > 0
text: root.statusMessage
font.pixelSize: 10
font.italic: true
color: root.isApplied
? Qt.rgba(0.2, 0.6, 0.2, 1)
: Qt.rgba(0.8, 0.2, 0.2, 1)
wrapMode: Text.WordWrap
}
}
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import Qt.labs.platform as Platform
TextEdit {
id: root
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
onLinkActivated: (link) => Qt.openUrlExternally(link)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
enabled: root.selectedText.length > 0
onTriggered: root.copy()
}
Platform.MenuItem {
text: qsTr("Select All")
enabled: root.text.length > 0
onTriggered: root.selectAll()
}
}
}

View File

@ -0,0 +1,183 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import Qt.labs.platform as Platform
Rectangle {
id: root
property string thinkingContent: ""
// property string signature: ""
property bool isRedacted: false
property bool expanded: false
property alias headerOpacity: headerRow.opacity
radius: 6
color: palette.base
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
MouseArea {
id: header
width: parent.width
height: headerRow.height + 10
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
width: parent.width
spacing: 8
Text {
text: root.isRedacted ? qsTr("Thinking (Redacted)")
: qsTr("Thinking")
font.pixelSize: 13
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
Column {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
spacing: 8
Text {
visible: root.isRedacted
width: parent.width
text: qsTr("Thinking content was redacted by safety systems")
font.pixelSize: 11
font.italic: true
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
wrapMode: Text.WordWrap
}
TextEdit {
id: thinkingText
visible: !root.isRedacted
width: parent.width
text: root.thinkingContent
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
// Rectangle {
// visible: root.signature.length > 0 && root.expanded
// width: parent.width
// height: signatureText.height + 10
// color: palette.alternateBase
// radius: 4
// Text {
// id: signatureText
// anchors {
// left: parent.left
// right: parent.right
// verticalCenter: parent.verticalCenter
// margins: 5
// }
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
// font.pixelSize: 9
// font.family: "monospace"
// color: palette.mid
// elide: Text.ElideRight
// }
// }
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: thinkingMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
: (root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
: Qt.lighter(palette.alternateBase, 1.3))
radius: root.radius
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: header.height
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + contentColumn.height + 20
}
}
]
}

View File

@ -0,0 +1,162 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import Qt.labs.platform as Platform
Rectangle {
id: root
property string toolContent: ""
property bool expanded: false
property alias headerOpacity: headerRow.opacity
readonly property int firstNewline: toolContent.indexOf('\n')
readonly property string toolName: firstNewline > 0 ? toolContent.substring(0, firstNewline) : toolContent
readonly property string toolResult: firstNewline > 0 ? toolContent.substring(firstNewline + 1) : ""
radius: 6
color: palette.base
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
MouseArea {
id: header
width: parent.width
height: headerRow.height + 10
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
width: parent.width
spacing: 8
Text {
text: qsTr("Tool: %1").arg(root.toolName)
font.pixelSize: 13
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
Column {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
spacing: 8
TextEdit {
id: resultText
width: parent.width
text: root.toolResult
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
enabled: resultText.selectedText.length > 0
onTriggered: resultText.copy()
}
Platform.MenuItem {
text: qsTr("Select All")
enabled: resultText.text.length > 0
onTriggered: resultText.selectAll()
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: messageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
: Qt.lighter(palette.alternateBase, 1.3)
radius: root.radius
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: header.height
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + contentColumn.height + 20
}
}
]
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -40,19 +40,68 @@ Flow {
Repeater {
id: attachRepeater
delegate: Rectangle {
delegate: FileItem {
id: fileItem
required property int index
required property string modelData
filePath: modelData
height: 30
width: contentRow.width + 10
radius: 4
color: palette.button
border.width: 1
border.color: mouse.hovered ? palette.highlight : root.accentColor
HoverHandler {
Rectangle {
anchors.fill: parent
radius: 4
color: palette.button
border.width: 1
border.color: mouse.containsMouse ? palette.highlight : root.accentColor
}
MouseArea {
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()
}
}
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.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 {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -21,14 +21,15 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
import UIControls
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias stopButton: stopButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias attachImages: attachImagesId
property alias linkFiles: linkFilesId
color: palette.window.hslLightness > 0.5 ?
@ -51,13 +52,12 @@ Rectangle {
QoAButton {
id: sendButtonId
text: qsTr("Send")
}
QoAButton {
id: stopButtonId
text: qsTr("Stop")
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
}
QoAButton {
@ -68,7 +68,22 @@ Rectangle {
height: 15
width: 8
}
text: qsTr("Attach files")
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 {
@ -79,7 +94,9 @@ Rectangle {
height: 15
width: 8
}
text: qsTr("Link files")
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Link file to context")
}
CheckBox {

View File

@ -0,0 +1,161 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import UIControls
Rectangle {
id: root
property int totalEdits: 0
property int appliedEdits: 0
property int pendingEdits: 0
property int rejectedEdits: 0
property bool hasAppliedEdits: appliedEdits > 0
property bool hasRejectedEdits: rejectedEdits > 0
property bool hasPendingEdits: pendingEdits > 0
signal applyAllClicked()
signal undoAllClicked()
visible: totalEdits > 0
implicitHeight: visible ? 40 : 0
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.05) :
Qt.lighter(palette.window, 1.05)
border.width: 1
border.color: palette.mid
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.InOutQuad
}
}
RowLayout {
anchors {
left: parent.left
leftMargin: 10
right: parent.right
rightMargin: 10
verticalCenter: parent.verticalCenter
}
spacing: 10
Rectangle {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
radius: 12
color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.2)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.2)
return Qt.rgba(0.8, 0.6, 0.2, 0.2)
}
border.width: 2
border.color: {
if (root.hasPendingEdits) return Qt.rgba(0.2, 0.6, 1.0, 0.8)
if (root.hasAppliedEdits) return Qt.rgba(0.2, 0.8, 0.2, 0.8)
return Qt.rgba(0.8, 0.6, 0.2, 0.8)
}
Text {
anchors.centerIn: parent
text: root.totalEdits
font.pixelSize: 10
font.bold: true
color: palette.text
}
}
// Status text
ColumnLayout {
spacing: 2
Text {
text: root.totalEdits === 1
? qsTr("File Edit in Current Message")
: qsTr("%1 File Edits in Current Message").arg(root.totalEdits)
font.pixelSize: 11
font.bold: true
color: palette.text
}
Text {
visible: root.totalEdits > 0
text: {
let parts = [];
if (root.appliedEdits > 0) {
parts.push(qsTr("%1 applied").arg(root.appliedEdits));
}
if (root.pendingEdits > 0) {
parts.push(qsTr("%1 pending").arg(root.pendingEdits));
}
if (root.rejectedEdits > 0) {
parts.push(qsTr("%1 rejected").arg(root.rejectedEdits));
}
return parts.join(", ");
}
font.pixelSize: 9
color: palette.mid
}
}
Item {
Layout.fillWidth: true
}
QoAButton {
id: applyAllButton
visible: root.hasPendingEdits || root.hasRejectedEdits
enabled: root.hasPendingEdits || root.hasRejectedEdits
text: root.hasPendingEdits
? qsTr("Apply All (%1)").arg(root.pendingEdits + root.rejectedEdits)
: qsTr("Reapply All (%1)").arg(root.rejectedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.hasPendingEdits
? qsTr("Apply all pending and rejected edits in this message")
: qsTr("Reapply all rejected edits in this message")
onClicked: root.applyAllClicked()
}
QoAButton {
id: undoAllButton
visible: root.hasAppliedEdits
enabled: root.hasAppliedEdits
text: qsTr("Undo All (%1)").arg(root.appliedEdits)
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Undo all applied edits in this message")
onClicked: root.undoAllClicked()
}
}
}

View File

@ -0,0 +1,251 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property var activeRules
property alias rulesCurrentIndex: rulesList.currentIndex
property alias ruleContentAreaText: ruleContentArea.text
signal refreshRules()
signal openRulesFolder()
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Active Project Rules")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Open Folder")
onClicked: root.openRulesFolder()
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
SplitView {
Layout.fillWidth: true
Layout.fillHeight: true
orientation: Qt.Horizontal
Rectangle {
SplitView.minimumWidth: 200
SplitView.preferredWidth: parent.width * 0.3
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules Files (%1)").arg(rulesList.count)
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted) {
return palette.highlight
} else if (parent.hovered) {
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
}
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 11
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: qsTr("Category: %1").arg(modelData.category)
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
}
}
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
}
Text {
visible: rulesList.count === 0
text: qsTr("No rules found.\nCreate .md files in:\n.qodeassist/rules/common/\n.qodeassist/rules/chat/")
font.pixelSize: 10
color: palette.mid
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignCenter
}
}
}
Rectangle {
SplitView.fillWidth: true
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: ruleContentArea.text.length > 0
onClicked: utils.copyToClipboard(ruleContentArea.text)
}
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
TextEdit {
id: ruleContentArea
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
}
}
}
}
Text {
text: qsTr("Rules are loaded from .qodeassist/rules/ directory in your project.\n" +
"Common rules apply to all contexts, chat rules apply only to chat assistant.")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
import QtQuick.Controls
Item {
id: root
signal filesDroppedToAttach(var urlStrings) // Array of URL strings (file://...)
signal filesDroppedToLink(var urlStrings) // Array of URL strings (file://...)
property string activeZone: ""
Item {
id: splitDropOverlay
anchors.fill: parent
visible: false
z: 999
Rectangle {
anchors.fill: parent
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
}
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
}
}
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
}
}
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) {
splitDropOverlay.visible = true
root.activeZone = ""
}
}
onExited: {
splitDropOverlay.visible = false
root.activeZone = ""
}
onPositionChanged: (drag) => {
if (drag.x < globalDropArea.width / 2) {
root.activeZone = "left"
} else {
root.activeZone = "right"
}
}
onDropped: (drop) => {
var targetZone = root.activeZone
splitDropOverlay.visible = false
root.activeZone = ""
if (drop.hasUrls && drop.urls.length > 0) {
// Convert URLs to array of strings for C++ processing
var urlStrings = []
for (var i = 0; i < drop.urls.length; i++) {
urlStrings.push(drop.urls[i].toString())
}
if (targetZone === "right") {
root.filesDroppedToLink(urlStrings)
} else {
root.filesDroppedToAttach(urlStrings)
}
}
}
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
import QtQuick
Rectangle {
id: root
property alias toastTextItem: textItem
property alias toastTextColor: textItem.color
property string errorText: ""
property int displayDuration: 7000
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
height: visible ? (textItem.implicitHeight + 12) : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
color: "#d32f2f"
radius: height / 2
border.color: "#b71c1c"
border.width: 1
visible: false
opacity: 0
TextEdit {
id: textItem
anchors.centerIn: parent
anchors.margins: 6
text: root.errorText
color: palette.text
font.pixelSize: 13
wrapMode: TextEdit.Wrap
width: Math.min(implicitWidth, root.parent.width - 60)
horizontalAlignment: TextEdit.AlignHCenter
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: "#b71c1c"
}
function show(message) {
errorText = message
visible = true
showAnimation.start()
hideTimer.restart()
}
function hide() {
hideAnimation.start()
}
NumberAnimation {
id: showAnimation
target: root
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
id: hideAnimation
target: root
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InQuad
onFinished: root.visible = false
}
Timer {
id: hideTimer
interval: root.displayDuration
running: false
repeat: false
onTriggered: root.hide()
}
}

View File

@ -0,0 +1,255 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
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 rulesButton: rulesButtonId
property alias agentModeSwitch: agentModeSwitchId
property alias thinkingMode: thinkingModeId
property alias activeRulesCount: activeRulesCountId.text
property alias configSelector: configSelectorId
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 {
height: agentModeSwitchId.height
spacing: 10
QoAButton {
id: pinButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
QoATextSlider {
id: agentModeSwitchId
anchors.verticalCenter: parent.verticalCenter
leftText: "chat"
rightText: "AI Agent"
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!agentModeSwitchId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return checked
? qsTr("Agent Mode: AI can use tools to read files, search project, and build code")
: qsTr("Chat Mode: Simple conversation without tool access")
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
}
Item {
height: agentModeSwitchId.height
width: recentPathId.width
Text {
id: recentPathId
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, root.width)
elide: Text.ElideMiddle
color: palette.text
font.pixelSize: 12
MouseArea {
anchors.fill: parent
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: recentPathId.text
}
}
}
RowLayout {
Layout.preferredWidth: root.width
spacing: 10
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: 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")
}
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")
}
QoAButton {
id: rulesButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/rules-icon.svg"
height: 15
width: 15
}
text: " "
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: root.activeRulesCount > 0
? qsTr("View active project rules (%1)").arg(root.activeRulesCount)
: qsTr("View active project rules (no rules found)")
Text {
id: activeRulesCountId
anchors {
bottom: parent.bottom
bottomMargin: 2
right: parent.right
rightMargin: 4
}
color: palette.text
font.pixelSize: 10
font.bold: true
}
}
Badge {
id: tokensBadgeId
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
}
QoAComboBox {
id: configSelectorId
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch AI configuration")
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,8 +19,8 @@
#include "ConfigurationManager.hpp"
#include <QTimer>
#include <settings/ButtonAspect.hpp>
#include <QTimer>
#include "QodeAssisttr.h"
@ -35,6 +35,52 @@ ConfigurationManager &ConfigurationManager::instance()
void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (!templ) {
return;
}
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplateDescription.setValue(templ->description());
} else if (&templateAspect == &m_generalSettings.qrTemplate) {
m_generalSettings.qrTemplateDescription.setValue(templ->description());
}
}
void ConfigurationManager::updateAllTemplateDescriptions()
{
updateTemplateDescription(m_generalSettings.ccTemplate);
updateTemplateDescription(m_generalSettings.caTemplate);
updateTemplateDescription(m_generalSettings.qrTemplate);
}
void ConfigurationManager::checkTemplate(const Utils::StringAspect &templateAspect)
{
LLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
@ -51,12 +97,16 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccSelectProvider, &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.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.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.caSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(&m_generalSettings.qrSetUrl, &Button::clicked, this, &Config::selectUrl);
connect(
&m_generalSettings.ccPreset1SelectProvider, &Button::clicked, this, &Config::selectProvider);
@ -64,6 +114,18 @@ void ConfigurationManager::setupConnections()
connect(&m_generalSettings.ccPreset1SelectModel, &Button::clicked, this, &Config::selectModel);
connect(
&m_generalSettings.ccPreset1SelectTemplate, &Button::clicked, this, &Config::selectTemplate);
connect(&m_generalSettings.ccTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.ccTemplate);
});
connect(&m_generalSettings.caTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.caTemplate);
});
connect(&m_generalSettings.qrTemplate, &Utils::StringAspect::changed, this, [this]() {
updateTemplateDescription(m_generalSettings.qrTemplate);
});
}
void ConfigurationManager::selectProvider()
@ -78,13 +140,13 @@ void ConfigurationManager::selectProvider()
? m_generalSettings.ccProvider
: settingsButton == &m_generalSettings.ccPreset1SelectProvider
? m_generalSettings.ccPreset1Provider
: settingsButton == &m_generalSettings.qrSelectProvider
? m_generalSettings.qrProvider
: m_generalSettings.caProvider;
QTimer::singleShot(0, this, [this, providersList, &targetSettings] {
m_generalSettings.showSelectionDialog(providersList,
targetSettings,
Tr::tr("Select LLM Provider"),
Tr::tr("Providers:"));
m_generalSettings.showSelectionDialog(
providersList, targetSettings, Tr::tr("Select LLM Provider"), Tr::tr("Providers:"));
});
}
@ -96,17 +158,21 @@ void ConfigurationManager::selectModel()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectModel);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectModel);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectModel);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
const auto providerUrl = isCodeCompletion ? m_generalSettings.ccUrl.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Url.volatileValue()
: isQuickRefactor ? m_generalSettings.qrUrl.volatileValue()
: m_generalSettings.caUrl.volatileValue();
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccModel
: isPreset1 ? m_generalSettings.ccPreset1Model
: isQuickRefactor ? m_generalSettings.qrModel
: m_generalSettings.caModel;
if (auto provider = m_providersManager.getProviderByName(providerName)) {
@ -137,19 +203,25 @@ void ConfigurationManager::selectTemplate()
const bool isCodeCompletion = (settingsButton == &m_generalSettings.ccSelectTemplate);
const bool isPreset1 = (settingsButton == &m_generalSettings.ccPreset1SelectTemplate);
const bool isQuickRefactor = (settingsButton == &m_generalSettings.qrSelectTemplate);
const QString providerName = isCodeCompletion ? m_generalSettings.ccProvider.volatileValue()
: isPreset1 ? m_generalSettings.ccPreset1Provider.volatileValue()
: isQuickRefactor ? m_generalSettings.qrProvider.volatileValue()
: m_generalSettings.caProvider.volatileValue();
auto providerID = m_providersManager.getProviderByName(providerName)->providerID();
const auto templateList = isCodeCompletion || isPreset1 ? m_templateManger.fimTemplatesNames()
: m_templateManger.chatTemplatesNames();
const auto templateList = isCodeCompletion || isPreset1
? m_templateManger.getFimTemplatesForProvider(providerID)
: m_templateManger.getChatTemplatesForProvider(providerID);
auto &targetSettings = isCodeCompletion ? m_generalSettings.ccTemplate
: isPreset1 ? m_generalSettings.ccPreset1Template
: isQuickRefactor ? m_generalSettings.qrTemplate
: m_generalSettings.caTemplate;
QTimer::singleShot(0, &m_generalSettings, [this, templateList, &targetSettings]() {
m_generalSettings.showSelectionDialog(templateList,
targetSettings,
Tr::tr("Select Template"),
Tr::tr("Templates:"));
m_generalSettings.showSelectionDialog(
templateList, targetSettings, Tr::tr("Select Template"), Tr::tr("Templates:"));
});
}
@ -169,6 +241,8 @@ void ConfigurationManager::selectUrl()
auto &targetSettings = (settingsButton == &m_generalSettings.ccSetUrl) ? m_generalSettings.ccUrl
: settingsButton == &m_generalSettings.ccPreset1SetUrl
? m_generalSettings.ccPreset1Url
: settingsButton == &m_generalSettings.qrSetUrl
? m_generalSettings.qrUrl
: m_generalSettings.caUrl;
QTimer::singleShot(0, &m_generalSettings, [this, urls, &targetSettings]() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of Qode Assist.
* 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
@ -24,17 +24,26 @@
#include "QodeAssistClient.hpp"
#include <QApplication>
#include <QInputDialog>
#include <QKeyEvent>
#include <QTimer>
#include <coreplugin/icore.h>
#include <languageclient/languageclientsettings.h>
#include <projectexplorer/projectmanager.h>
#include "LLMClientInterface.hpp"
#include "LLMSuggestion.hpp"
#include "RefactorSuggestion.hpp"
#include "RefactorSuggestionHoverHandler.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include "settings/ProjectSettings.hpp"
#include "settings/QuickRefactorSettings.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
@ -44,11 +53,12 @@ using namespace Core;
namespace QodeAssist {
QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(new LLMClientInterface())
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
, m_recentCharCount(0)
{
setName("Qode Assist");
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
@ -57,11 +67,20 @@ QodeAssistClient::QodeAssistClient()
setupConnections();
m_typingTimer.start();
m_hintHideTimer.setSingleShot(true);
m_hintHideTimer.setInterval(Settings::codeCompletionSettings().hintHideTimeout());
connect(&m_hintHideTimer, &QTimer::timeout, this, [this]() { m_hintHandler.hideHint(); });
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
QodeAssistClient::~QodeAssistClient()
{
cleanupConnections();
delete m_refactorHoverHandler;
delete m_refactorWidgetHandler;
}
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
@ -71,6 +90,15 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return;
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(
document,
&TextDocument::contentsChangedWithPosition,
@ -102,6 +130,12 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return;
}
@ -112,20 +146,35 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
if (lastChar.isEmpty() || lastChar[0].isPunct()) {
m_recentCharCount = 0;
m_typingTimer.restart();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
return;
}
m_recentCharCount += charsAdded;
bool isSpaceOrTab = lastChar[0].isSpace();
bool ignoreWhitespace
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
if (!ignoreWhitespace || !isSpaceOrTab) {
m_recentCharCount += charsAdded;
}
if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_recentCharCount = charsAdded;
m_recentCharCount = (ignoreWhitespace && isSpaceOrTab) ? 0 : charsAdded;
m_typingTimer.restart();
}
if (m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
scheduleRequest(widget);
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode == 1) {
handleAutoRequestTrigger(widget, charsAdded, isSpaceOrTab);
} else {
handleHintBasedTrigger(widget, charsAdded, isSpaceOrTab, cursor);
}
});
}
@ -142,14 +191,38 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
MultiTextCursor cursor = editor->multiTextCursor();
if (cursor.hasMultipleCursors() || cursor.hasSelection() || editor->suggestionVisible())
return;
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 0) {
editor->abortAssist();
}
const FilePath filePath = editor->textDocument()->filePath();
GetCompletionRequest request{{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
Position(cursor.mainCursor())}};
GetCompletionRequest request{
{TextDocumentIdentifier(hostPathToServerUri(filePath)),
documentVersion(filePath),
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)](
const GetCompletionRequest::Response &response) {
QTC_ASSERT(editor, return);
@ -159,6 +232,41 @@ void QodeAssistClient::requestCompletions(TextEditor::TextEditorWidget *editor)
sendMessage(request);
}
void QodeAssistClient::requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
auto project = ProjectManager::projectForFile(editor->textDocument()->filePath());
if (!isEnabled(project))
return;
if (m_llmClient->contextManager()
->ignoreManager()
->shouldIgnore(editor->textDocument()->filePath().toUrlishString(), project)) {
LOG_MESSAGE(QString("Ignoring file due to .qodeassistignore: %1")
.arg(editor->textDocument()->filePath().toUrlishString()));
return;
}
if (!m_refactorHandler) {
m_refactorHandler = new QuickRefactorHandler(this);
connect(
m_refactorHandler,
&QuickRefactorHandler::refactoringCompleted,
this,
&QodeAssistClient::handleRefactoringResult);
}
m_progressHandler.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)
{
cancelRunningRequest(editor);
@ -181,6 +289,12 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
});
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
cancelRunningRequest(editor);
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
});
it = m_scheduledRequests.insert(editor, timer);
}
@ -188,11 +302,22 @@ void QodeAssistClient::scheduleRequest(TextEditor::TextEditorWidget *editor)
it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::codeCompletionSettings().startSuggestionTimer());
}
void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &response,
TextEditor::TextEditorWidget *editor)
void QodeAssistClient::handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor)
{
if (response.error())
m_progressHandler.hideProgress();
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (Settings::codeCompletionSettings().abortAssistOnRequest() && triggerMode == 1) {
editor->abortAssist();
}
if (response.error()) {
log(*response.error());
m_errorHandler
.showError(editor, tr("Code completion failed: %1").arg(response.error()->message()));
return;
}
int requestPosition = -1;
if (const auto requestParams = m_runningRequests.take(editor).params())
@ -212,14 +337,13 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
// remove trailing whitespaces from the end of the completions
for (Completion &completion : completions) {
const LanguageServerProtocol::Range range = completion.range();
if (range.start().line() != range.end().line())
continue; // do not remove trailing whitespaces for multi-line replacements
continue;
const 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;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
@ -236,8 +360,11 @@ void QodeAssistClient::handleCompletions(const GetCompletionRequest::Response &r
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
if (completions.isEmpty())
if (completions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
}
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
}
}
@ -247,6 +374,13 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
// 0 = Hint-based, 1 = Automatic
const int triggerMode = Settings::codeCompletionSettings().completionTriggerMode();
if (triggerMode != 1) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
cancelRequest(it->id());
m_runningRequests.erase(it);
}
@ -267,18 +401,13 @@ void QodeAssistClient::setupConnections()
openDocument(textDocument);
};
m_documentOpenedConnection = connect(EditorManager::instance(),
&EditorManager::documentOpened,
this,
openDoc);
m_documentClosedConnection = connect(EditorManager::instance(),
&EditorManager::documentClosed,
this,
[this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(
document))
closeDocument(textDocument);
});
m_documentOpenedConnection
= connect(EditorManager::instance(), &EditorManager::documentOpened, this, openDoc);
m_documentClosedConnection = connect(
EditorManager::instance(), &EditorManager::documentClosed, this, [this](IDocument *document) {
if (auto textDocument = qobject_cast<TextDocument *>(document))
closeDocument(textDocument);
});
for (IDocument *doc : DocumentModel::openedDocuments())
openDoc(doc);
@ -293,4 +422,300 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear();
}
bool QodeAssistClient::isHintVisible() const
{
return m_hintHandler.isHintVisible();
}
void QodeAssistClient::hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor)
{
m_hintHandler.hideHint();
requestCompletions(editor);
}
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);
}
}
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
} // anonymous namespace
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)};
QString originalText;
const int startPos = range.begin.toPositionInDocument(editorWidget->document());
const int endPos = range.end.toPositionInDocument(editorWidget->document());
if (startPos != endPos) {
QTextCursor cursor(editorWidget->document());
cursor.setPosition(startPos);
cursor.setPosition(endPos, QTextCursor::KeepAnchor);
originalText = cursor.selectedText();
originalText.replace(QChar(0x2029), "\n");
}
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
const Utils::Text::Range range{
Utils::Text::Position{result.insertRange.begin.line, result.insertRange.begin.column},
Utils::Text::Position{result.insertRange.end.line, result.insertRange.end.column}};
const QTextCursor startCursor = range.begin.toTextCursor(editorWidget->document());
const QTextCursor endCursor = range.end.toTextCursor(editorWidget->document());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(editorWidget->document());
editCursor.beginEditBlock();
if (startPos == endPos) {
editCursor.setPosition(startPos);
editCursor.insertText(editedText);
} else {
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
editCursor.insertText(editedText);
}
editCursor.endEditBlock();
LOG_MESSAGE("Refactoring applied via widget with edited text");
});
m_refactorWidgetHandler->setDeclineCallback([this]() {
LOG_MESSAGE("Refactoring declined via widget");
});
m_refactorWidgetHandler->showRefactorWidget(
editorWidget,
originalText,
result.newText,
range);
LOG_MESSAGE(QString("Displaying refactoring widget - Original: %1 chars, New: %2 chars")
.arg(originalText.length())
.arg(result.newText.length()));
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget,
int charsAdded,
bool isSpaceOrTab)
{
Q_UNUSED(isSpaceOrTab);
if (m_recentCharCount
> Settings::codeCompletionSettings().autoCompletionCharThreshold()) {
scheduleRequest(widget);
}
}
void QodeAssistClient::handleHintBasedTrigger(TextEditor::TextEditorWidget *widget,
int charsAdded,
bool isSpaceOrTab,
QTextCursor &cursor)
{
Q_UNUSED(charsAdded);
const int hintThreshold = Settings::codeCompletionSettings().hintCharThreshold();
if (m_recentCharCount >= hintThreshold && !isSpaceOrTab) {
const QRect cursorRect = widget->cursorRect(cursor);
QPoint globalPos = widget->viewport()->mapToGlobal(cursorRect.topLeft());
QPoint localPos = widget->mapFromGlobal(globalPos);
int fontSize = widget->font().pixelSize();
if (fontSize <= 0) {
fontSize = widget->fontMetrics().height();
}
QTextCursor textCursor = widget->textCursor();
if (m_recentCharCount <= hintThreshold) {
textCursor
.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, m_recentCharCount);
} else {
textCursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, hintThreshold);
}
int x = localPos.x() + cursorRect.height();
int y = localPos.y() + cursorRect.height() / 4;
QPoint hintPos(x, y);
if (!m_hintHandler.isHintVisible()) {
m_hintHandler.showHint(widget, hintPos, fontSize);
} else {
m_hintHandler.updateHintPosition(widget, hintPos);
}
m_hintHideTimer.start();
}
}
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);
// Check hint trigger key (0=Space, 1=Ctrl+Space, 2=Alt+Space, 3=Ctrl+Enter, 4=Tab, 5=Enter)
if (m_hintHandler.isHintVisible()) {
const int triggerKeyIndex = Settings::codeCompletionSettings().hintTriggerKey();
bool isMatchingKey = false;
const Qt::KeyboardModifiers modifiers = keyEvent->modifiers();
switch (triggerKeyIndex) {
case 0: // Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
break;
case 1: // Ctrl+Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers & Qt::ControlModifier));
break;
case 2: // Alt+Space
isMatchingKey = (keyEvent->key() == Qt::Key_Space
&& (modifiers & Qt::AltModifier));
break;
case 3: // Ctrl+Enter
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
&& (modifiers & Qt::ControlModifier));
break;
case 4: // Tab
isMatchingKey = (keyEvent->key() == Qt::Key_Tab);
break;
case 5: // Enter
isMatchingKey = ((keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Enter)
&& (modifiers == Qt::NoModifier || modifiers == Qt::ShiftModifier));
break;
}
if (isMatchingKey) {
m_hintHideTimer.stop();
m_hintHandler.hideHint();
requestCompletions(editor);
return true;
}
}
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();
m_hintHideTimer.stop();
m_hintHandler.hideHint();
}
}
return LanguageClient::Client::eventFilter(watched, event);
}
} // namespace QodeAssist

View File

@ -1,8 +1,8 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of Qode Assist.
* 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
@ -24,32 +24,58 @@
#pragma once
#include <languageclient/client.h>
#include <QObject>
#include "LLMClientInterface.hpp"
#include "LSPCompletion.hpp"
#include "QuickRefactorHandler.hpp"
#include "RefactorSuggestionHoverHandler.hpp"
#include "widgets/CompletionProgressHandler.hpp"
#include "widgets/CompletionErrorHandler.hpp"
#include "widgets/CompletionHintHandler.hpp"
#include "widgets/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include <llmcore/IPromptProvider.hpp>
#include <llmcore/IProviderRegistry.hpp>
namespace QodeAssist {
class QodeAssistClient : public LanguageClient::Client
{
Q_OBJECT
public:
explicit QodeAssistClient();
explicit QodeAssistClient(LLMClientInterface *clientInterface);
~QodeAssistClient() override;
void openDocument(TextEditor::TextDocument *document) override;
bool canOpenProject(ProjectExplorer::Project *project) override;
void requestCompletions(TextEditor::TextEditorWidget *editor);
void requestQuickRefactor(
TextEditor::TextEditorWidget *editor, const QString &instructions = QString());
bool isHintVisible() const;
void hideHintAndRequestCompletion(TextEditor::TextEditorWidget *editor);
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private:
void scheduleRequest(TextEditor::TextEditorWidget *editor);
void handleCompletions(const GetCompletionRequest::Response &response,
TextEditor::TextEditorWidget *editor);
void handleCompletions(
const GetCompletionRequest::Response &response, TextEditor::TextEditorWidget *editor);
void cancelRunningRequest(TextEditor::TextEditorWidget *editor);
bool isEnabled(ProjectExplorer::Project *project) const;
void setupConnections();
void cleanupConnections();
void handleRefactoringResult(const RefactorResult &result);
void displayRefactoringSuggestion(const RefactorResult &result);
void displayRefactoringWidget(const RefactorResult &result);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab);
void handleHintBasedTrigger(TextEditor::TextEditorWidget *widget, int charsAdded, bool isSpaceOrTab, QTextCursor &cursor);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@ -58,6 +84,15 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
QTimer m_hintHideTimer;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
CompletionHintHandler m_hintHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

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

View File

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

View File

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

415
QuickRefactorHandler.cpp Normal file
View File

@ -0,0 +1,415 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "QuickRefactorHandler.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
#include <context/DocumentContextReader.hpp>
#include <context/DocumentReaderQtCreator.hpp>
#include <context/Utils.hpp>
#include <llmcore/PromptTemplateManager.hpp>
#include <llmcore/ProvidersManager.hpp>
#include <llmcore/RequestConfig.hpp>
#include <llmcore/RulesLoader.hpp>
#include <logger/Logger.hpp>
#include <settings/ChatAssistantSettings.hpp>
#include <settings/GeneralSettings.hpp>
#include <settings/QuickRefactorSettings.hpp>
namespace QodeAssist {
QuickRefactorHandler::QuickRefactorHandler(QObject *parent)
: QObject(parent)
, m_currentEditor(nullptr)
, m_isRefactoringInProgress(false)
, m_contextManager(this)
{
}
QuickRefactorHandler::~QuickRefactorHandler() {}
void QuickRefactorHandler::sendRefactorRequest(
TextEditor::TextEditorWidget *editor, const QString &instructions)
{
if (m_isRefactoringInProgress) {
cancelRequest();
}
m_currentEditor = editor;
Utils::Text::Range range;
if (editor->textCursor().hasSelection()) {
QTextCursor cursor = editor->textCursor();
int startPos = cursor.selectionStart();
int endPos = cursor.selectionEnd();
QTextBlock startBlock = editor->document()->findBlock(startPos);
int startLine = startBlock.blockNumber() + 1;
int startColumn = startPos - startBlock.position();
QTextBlock endBlock = editor->document()->findBlock(endPos);
int endLine = endBlock.blockNumber() + 1;
int endColumn = endPos - endBlock.position();
Utils::Text::Position startPosition;
startPosition.line = startLine;
startPosition.column = startColumn;
Utils::Text::Position endPosition;
endPosition.line = endLine;
endPosition.column = endColumn;
range = Utils::Text::Range();
range.begin = startPosition;
range.end = endPosition;
} else {
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
QTextBlock block = editor->document()->findBlock(cursorPos);
int line = block.blockNumber() + 1;
int column = cursorPos - block.position();
Utils::Text::Position cursorPosition;
cursorPosition.line = line;
cursorPosition.column = column;
range = Utils::Text::Range();
range.begin = cursorPosition;
range.end = cursorPosition;
}
m_currentRange = range;
prepareAndSendRequest(editor, instructions, range);
}
void QuickRefactorHandler::prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range)
{
auto &settings = Settings::generalSettings();
auto &providerRegistry = LLMCore::ProvidersManager::instance();
auto &promptManager = LLMCore::PromptTemplateManager::instance();
const auto providerName = settings.qrProvider();
auto provider = providerRegistry.getProviderByName(providerName);
if (!provider) {
QString error = QString("No provider found with name: %1").arg(providerName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return;
}
const auto templateName = settings.qrTemplate();
auto promptTemplate = promptManager.getChatTemplateByName(templateName);
if (!promptTemplate) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = editor;
emit refactoringCompleted(result);
return;
}
LLMCore::LLMConfig config;
config.requestType = LLMCore::RequestType::QuickRefactoring;
config.provider = provider;
config.promptTemplate = promptTemplate;
config.url = QString("%1%2").arg(settings.qrUrl(), provider->chatEndpoint());
config.apiKey = provider->apiKey();
if (provider->providerID() == LLMCore::ProviderID::GoogleAI) {
QString stream = QString{"streamGenerateContent?alt=sse"};
config.url = QUrl(QString("%1/models/%2:%3")
.arg(
Settings::generalSettings().qrUrl(),
Settings::generalSettings().qrModel(),
stream));
} else {
config.url
= QString("%1%2").arg(Settings::generalSettings().qrUrl(), provider->chatEndpoint());
config.providerRequest
= {{"model", Settings::generalSettings().qrModel()}, {"stream", true}};
}
LLMCore::ContextData context = prepareContext(editor, range, instructions);
bool enableTools = Settings::quickRefactorSettings().useTools();
bool enableThinking = Settings::quickRefactorSettings().useThinking();
provider->prepareRequest(
config.providerRequest,
promptTemplate,
context,
LLMCore::RequestType::QuickRefactoring,
enableTools,
enableThinking);
QString requestId = QUuid::createUuid().toString();
m_lastRequestId = requestId;
QJsonObject request{{"id", requestId}};
m_isRefactoringInProgress = true;
m_activeRequests[requestId] = {request, provider};
connect(
provider,
&LLMCore::Provider::fullResponseReceived,
this,
&QuickRefactorHandler::handleFullResponse,
Qt::UniqueConnection);
connect(
provider,
&LLMCore::Provider::requestFailed,
this,
&QuickRefactorHandler::handleRequestFailed,
Qt::UniqueConnection);
provider->sendRequest(requestId, config.url, config.providerRequest);
}
LLMCore::ContextData QuickRefactorHandler::prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions)
{
LLMCore::ContextData context;
auto textDocument = editor->textDocument();
Context::DocumentReaderQtCreator documentReader;
auto documentInfo = documentReader.readDocument(textDocument->filePath().toUrlishString());
if (!documentInfo.document) {
LOG_MESSAGE("Error: Document is not available");
return context;
}
QTextCursor cursor = editor->textCursor();
int cursorPos = cursor.position();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
QString taggedContent;
bool readFullFile = Settings::quickRefactorSettings().readFullFile();
if (cursor.hasSelection()) {
int selStart = cursor.selectionStart();
int selEnd = cursor.selectionEnd();
QTextBlock startBlock = documentInfo.document->findBlock(selStart);
int startLine = startBlock.blockNumber();
int startColumn = selStart - startBlock.position();
QTextBlock endBlock = documentInfo.document->findBlock(selEnd);
int endLine = endBlock.blockNumber();
int endColumn = selEnd - endBlock.position();
QString contextBefore;
if (readFullFile) {
contextBefore = reader.readWholeFileBefore(startLine, startColumn);
} else {
contextBefore = reader.getContextBefore(
startLine, startColumn, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
}
QString selectedText = cursor.selectedText();
selectedText.replace(QChar(0x2029), "\n");
QString contextAfter;
if (readFullFile) {
contextAfter = reader.readWholeFileAfter(endLine, endColumn);
} else {
contextAfter = reader.getContextAfter(
endLine, endColumn, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
}
taggedContent = contextBefore;
if (selStart == cursorPos) {
taggedContent += "<cursor><selection_start>" + selectedText + "<selection_end>";
} else {
taggedContent += "<selection_start>" + selectedText + "<selection_end><cursor>";
}
taggedContent += contextAfter;
} else {
QTextBlock block = documentInfo.document->findBlock(cursorPos);
int line = block.blockNumber();
int column = cursorPos - block.position();
QString contextBefore;
if (readFullFile) {
contextBefore = reader.readWholeFileBefore(line, column);
} else {
contextBefore = reader.getContextBefore(
line, column, Settings::quickRefactorSettings().readStringsBeforeCursor() + 1);
}
QString contextAfter;
if (readFullFile) {
contextAfter = reader.readWholeFileAfter(line, column);
} else {
contextAfter = reader.getContextAfter(
line, column, Settings::quickRefactorSettings().readStringsAfterCursor() + 1);
}
taggedContent = contextBefore + "<cursor>" + contextAfter;
}
QString systemPrompt = Settings::quickRefactorSettings().systemPrompt();
auto project = LLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules = LLMCore::RulesLoader::loadRulesForProject(
project, LLMCore::RulesContext::QuickRefactor);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for quick refactor");
}
}
systemPrompt += "\n\nFile information:";
systemPrompt += "\nLanguage: " + documentInfo.mimeType;
systemPrompt += "\nFile path: " + documentInfo.filePath;
systemPrompt += "\n\nCode context with position markers:";
systemPrompt += taggedContent;
systemPrompt += "\n\nOutput format:";
systemPrompt += "\n- Generate ONLY the code that should replace the current selection "
"between<selection_start><selection_end> or be "
"inserted at cursor position<cursor>";
systemPrompt += "\n- Do not include any explanations, comments about the code, or markdown "
"code block markers";
systemPrompt += "\n- The output should be ready to insert directly into the editor";
systemPrompt += "\n- Follow the existing code style and indentation patterns";
if (Settings::codeCompletionSettings().useOpenFilesInQuickRefactor()) {
systemPrompt += "\n\n" + m_contextManager.openedFilesContext({documentInfo.filePath});
}
context.systemPrompt = systemPrompt;
QVector<LLMCore::Message> messages;
messages.append(
{"user",
instructions.isEmpty() ? "Refactor the code to improve its quality and maintainability."
: instructions});
context.history = messages;
return context;
}
void QuickRefactorHandler::handleLLMResponse(
const QString &response, const QJsonObject &request, bool isComplete)
{
if (request["id"].toString() != m_lastRequestId) {
return;
}
if (isComplete) {
m_isRefactoringInProgress = false;
QString cleanedResponse = response.trimmed();
if (cleanedResponse.startsWith("```")) {
int firstNewLine = cleanedResponse.indexOf('\n');
int lastFence = cleanedResponse.lastIndexOf("```");
if (firstNewLine != -1 && lastFence > firstNewLine) {
cleanedResponse
= cleanedResponse.mid(firstNewLine + 1, lastFence - firstNewLine - 1).trimmed();
} else if (lastFence != -1) {
cleanedResponse = cleanedResponse.mid(3, lastFence - 3).trimmed();
}
}
RefactorResult result;
result.newText = cleanedResponse;
result.insertRange = m_currentRange;
result.success = true;
result.editor = m_currentEditor;
LOG_MESSAGE("Refactoring completed successfully. New code to insert: ");
LOG_MESSAGE("---------- BEGIN REFACTORED CODE ----------");
LOG_MESSAGE(cleanedResponse);
LOG_MESSAGE("----------- END REFACTORED CODE -----------");
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::cancelRequest()
{
if (m_isRefactoringInProgress) {
auto id = m_lastRequestId;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.key() == id) {
const RequestContext &ctx = it.value();
ctx.provider->cancelRequest(id);
m_activeRequests.erase(it);
break;
}
}
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = "Refactoring request was cancelled";
emit refactoringCompleted(result);
}
}
void QuickRefactorHandler::handleFullResponse(const QString &requestId, const QString &fullText)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
QJsonObject request{{"id", requestId}};
handleLLMResponse(fullText, request, true);
}
}
void QuickRefactorHandler::handleRequestFailed(const QString &requestId, const QString &error)
{
if (requestId == m_lastRequestId) {
m_activeRequests.remove(requestId);
m_isRefactoringInProgress = false;
RefactorResult result;
result.success = false;
result.errorMessage = error;
result.editor = m_currentEditor;
emit refactoringCompleted(result);
}
}
} // namespace QodeAssist

90
QuickRefactorHandler.hpp Normal file
View File

@ -0,0 +1,90 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QJsonObject>
#include <QObject>
#include <texteditor/texteditor.h>
#include <utils/textutils.h>
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <llmcore/ContextData.hpp>
#include <llmcore/Provider.hpp>
namespace QodeAssist {
struct RefactorResult
{
QString newText;
Utils::Text::Range insertRange;
bool success;
QString errorMessage;
TextEditor::TextEditorWidget *editor{nullptr};
};
class QuickRefactorHandler : public QObject
{
Q_OBJECT
public:
explicit QuickRefactorHandler(QObject *parent = nullptr);
~QuickRefactorHandler() override;
void sendRefactorRequest(TextEditor::TextEditorWidget *editor, const QString &instructions);
void cancelRequest();
bool isProcessing() const { return m_isRefactoringInProgress; }
signals:
void refactoringCompleted(const QodeAssist::RefactorResult &result);
private slots:
void handleFullResponse(const QString &requestId, const QString &fullText);
void handleRequestFailed(const QString &requestId, const QString &error);
private:
void prepareAndSendRequest(
TextEditor::TextEditorWidget *editor,
const QString &instructions,
const Utils::Text::Range &range);
void handleLLMResponse(const QString &response, const QJsonObject &request, bool isComplete);
LLMCore::ContextData prepareContext(
TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &instructions);
struct RequestContext
{
QJsonObject originalRequest;
LLMCore::Provider *provider;
};
QHash<QString, RequestContext> m_activeRequests;
TextEditor::TextEditorWidget *m_currentEditor;
Utils::Text::Range m_currentRange;
bool m_isRefactoringInProgress;
QString m_lastRequestId;
Context::ContextManager m_contextManager;
};
} // namespace QodeAssist

520
README.md
View File

@ -2,10 +2,9 @@
[![Build plugin](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml/badge.svg?branch=main)](https://github.com/Palm1r/QodeAssist/actions/workflows/build_cmake.yml)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Palm1r/QodeAssist/total?color=41%2C173%2C71)
![GitHub Tag](https://img.shields.io/github/v/tag/Palm1r/QodeAssist)
![Static Badge](https://img.shields.io/badge/QtCreator-15.0.1-brightgreen)
![Static Badge](https://img.shields.io/badge/donations:0-brightgreen)
[![](https://dcbadge.limes.pink/api/server/BGMkUsXUgf?style=flat)](https://discord.gg/BGMkUsXUgf)
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is an AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion and suggestions for C++ and QML, leveraging large language models through local providers like Ollama. Enhance your coding productivity with context-aware AI assistance directly in your Qt development environment.
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d) QodeAssist is a comprehensive AI-powered coding assistant plugin for Qt Creator. It provides intelligent code completion, interactive chat with multiple interface options, inline quick refactoring, and AI function calling capabilities for C++ and QML development. Supporting both local providers (Ollama, llama.cpp, LM Studio) and cloud services (Claude, OpenAI, Google AI, Mistral AI), QodeAssist enhances your productivity with context-aware AI assistance, project-specific rules, and extensive customization options directly in your Qt development environment.
⚠️ **Important Notice About Paid Providers**
> When using paid providers like Claude, OpenRouter or OpenAI-compatible services:
@ -13,56 +12,43 @@
> - The QodeAssist developer bears no responsibility for any charges incurred
> - Please carefully review the provider's pricing and your account settings before use
⚠️ **Commercial Support and Custom Development**
> The QodeAssist developer offers commercial services for:
> - Adapting the plugin for specific Qt Creator versions
> - Custom development for particular operating systems
> - Integration with specific language models
> - Implementing custom features and modifications
>
> For commercial inquiries, please contact: qodeassist.dev@pm.me
## Table of Contents
1. [Overview](#overview)
2. [Install plugin to QtCreator](#install-plugin-to-qtcreator)
3. [Configure for Anthropic Claude](#configure-for-anthropic-claude)
4. [Configure for OpenAI](#configure-for-openai)
5. [Configure for using Ollama](#configure-for-using-ollama)
6. [System Prompt Configuration](#system-prompt-configuration)
7. [File Context Features](#file-context-features)
8. [Template-Model Compatibility](#template-model-compatibility)
9. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
10. [Development Progress](#development-progress)
11. [Hotkeys](#hotkeys)
12. [Troubleshooting](#troubleshooting)
13. [Support the Development](#support-the-development-of-qodeassist)
14. [How to Build](#how-to-build)
2. [Install Plugin](#install-plugin-to-qtcreator)
3. [Configuration](#configuration)
4. [Features](#features)
5. [Context Layers](#context-layers)
6. [QtCreator Version Compatibility](#qtcreator-version-compatibility)
7. [Hotkeys](#hotkeys)
8. [Troubleshooting](#troubleshooting)
9. [Development Progress](#development-progress)
10. [Support the Development](#support-the-development-of-qodeassist)
11. [How to Build](#how-to-build)
## Overview
- AI-powered code completion
- 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
- OpenAI-compatible providers(eg. https://openrouter.ai)
- Extensive library of model-specific templates
- Custom template support
- Easy configuration and model selection
QodeAssist enhances Qt Creator with AI-powered coding assistance:
- **Code Completion**: Intelligent, context-aware code suggestions for C++ and QML
- **Chat Assistant**: Multiple interface options (popup window, side panel, bottom panel)
- **Quick Refactoring**: Inline AI-assisted code improvements directly in editor with custom instructions library
- **File Context**: Attach or link files for better AI understanding
- **Tool Calling**: AI can read project files, search code, and access diagnostics
- **Multiple Providers**: Support for Ollama, Claude, OpenAI, Google AI, Mistral AI, llama.cpp, and more
- **Customizable**: Project-specific rules, custom instructions, and extensive model templates
**Join our [Discord Community](https://discord.gg/BGMkUsXUgf)** to get support and connect with other users!
<details>
<summary>Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Quick refactor in code: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/4a9092e0-429f-41eb-8723-cbb202fd0a8c" width="600" alt="QodeAssistPreview">
</details>
<details>
<summary>Multiline Code completion: (click to expand)</summary>
<img src="https://github.com/user-attachments/assets/c18dfbd2-8c54-4a7b-90d1-66e3bb51adb0" width="600" alt="QodeAssistPreview">
@ -78,15 +64,58 @@
<img width="326" alt="QodeAssistBottomPanel" src="https://github.com/user-attachments/assets/4cc64c23-a294-4df8-9153-39ad6fdab34b">
</details>
<details>
<summary>Chat in addtional window: (click to expand)</summary>
<img width="851" height="865" alt="image" src="https://github.com/user-attachments/assets/a68894b7-886e-4501-a61b-7161ae34b427" />
</details>
<details>
<summary>Automatic syncing with open editor files: (click to expand)</summary>
<img width="600" alt="OpenedDocumentsSync" src="https://github.com/user-attachments/assets/08efda2f-dc4d-44c3-927c-e6a975090d2f">
</details>
<details>
<summary>Example how tools works: (click to expand)</summary>
<img width="600" alt="ToolsDemo" src="https://github.com/user-attachments/assets/cf6273ad-d5c8-47fc-81e6-23d929547f6c">
</details>
## Install plugin to QtCreator
### 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
2. Download the QodeAssist plugin for your Qt Creator
- Remove old version plugin if already was installed
- on macOS for QtCreator 16: ~/Library/Application Support/QtProject/Qt Creator/plugins/16.0.0/petrmironychev.qodeassist
- on windows for QtCreator 16: C:\Users\<user>\AppData\Local\QtProject\qtcreator\plugins\16.0.0\petrmironychev.qodeassist\lib\qtcreator\plugins
3. Launch Qt Creator and install the plugin:
- Go to:
- MacOS: Qt Creator -> About Plugins...
@ -94,169 +123,263 @@
- Click on "Install Plugin..."
- Select the downloaded QodeAssist plugin archive file
## Configure for Anthropic Claude
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure Claude api key
3. Return to General tab and configure:
- Set "Claude" as the provider for code completion or/and chat assistant
- 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
## Configuration
QodeAssist supports multiple LLM providers. Choose your preferred provider and follow the configuration guide:
### Supported Providers
- **[Ollama](docs/ollama-configuration.md)** - Local LLM provider
- **[llama.cpp](docs/llamacpp-configuration.md)** - Local LLM server
- **[Anthropic Claude](docs/claude-configuration.md)** - Сloud provider
- **[OpenAI](docs/openai-configuration.md)** - Сloud provider
- **[Mistral AI](docs/mistral-configuration.md)** - Сloud provider
- **[Google AI](docs/google-ai-configuration.md)** - Сloud provider
- **LM Studio** - Local LLM provider
- **OpenAI-compatible** - Custom providers (OpenRouter, etc.)
### Additional Configuration
- **[Project Rules](docs/project-rules.md)** - Customize AI behavior for your project
- **[Ignoring Files](docs/ignoring-files.md)** - Exclude files from context using `.qodeassistignore`
## Features
### Code Completion
- AI-powered intelligent code completion
- Support for C++ and QML
- Context-aware suggestions
- Multiline completions
#### Completion Trigger Modes
QodeAssist offers two trigger modes for code completion:
**Hint-based (Default, Recommended)**
- 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**
- 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
- **[File Context](docs/file-context.md)** - Attach or link files for better context
- Automatic syncing with open editor files (optional)
- Extended thinking mode (Claude, other providers in plan) - Enable deeper reasoning for complex tasks
### Quick Refactoring
- Inline code refactoring directly in the editor with AI assistance
- Selection-based improvements with instant code replacement
- Built-in quick actions (repeat, improve, alternative)
- **Custom instructions library** with search and autocomplete
- Create, edit, and manage reusable refactoring templates
- Combine base instructions with specific details
- **[Learn more](docs/quick-refactoring.md)**
### Tools & Function Calling
- Read project files
- List and search in project
- Access linter/compiler issues
- Enabled by default (can be disabled)
## Context Layers
QodeAssist uses a flexible prompt composition system that adapts to different contexts. Here's how prompts are constructed for each feature:
<details>
<summary>Example of Claude settings: (click to expand)</summary>
<img width="823" alt="Claude Settings" src="https://github.com/user-attachments/assets/828e09ea-e271-4a7a-8271-d3d5dd5c13fd" />
<summary><strong>Code Completion (FIM Models)</strong> - Codestral, Qwen2.5-Coder, DeepSeek-Coder (click to expand)</summary>
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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>
## Configure for OpenAI
1. Open Qt Creator settings and navigate to the QodeAssist section
2. Go to Provider Settings tab and configure OpenAI api key
3. Return to General tab and configure:
- Set "OpenAI" as the provider for code completion or/and chat assistant
- Set the OpenAI URL (https://api.openai.com)
- Select your preferred model (e.g., gpt-4o)
- Choose the OpenAI template for code completion or/and chat
<details>
<summary>Example of OpenAI settings: (click to expand)</summary>
<img width="829" alt="OpenAI Settings" src="https://github.com/user-attachments/assets/4716f790-6159-44d0-a8f4-565ccb6eb713" />
<summary><strong>Code Completion (Non-FIM Models)</strong> - DeepSeek-Coder-Instruct, Qwen2.5-Coder-Instruct (click to expand)</summary>
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 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>
## Configure for using Ollama
1. Install [Ollama](https://ollama.com). Make sure to review the system requirements before installation.
2. Install a language models in Ollama via terminal. For example, you can run:
For standard computers (minimum 8GB RAM):
```
ollama run qwen2.5-coder:7b
```
For better performance (16GB+ RAM):
```
ollama run qwen2.5-coder:14b
```
For high-end systems (32GB+ RAM):
```
ollama run qwen2.5-coder:32b
```
1. Open Qt Creator settings (Edit > Preferences on Linux/Windows, Qt Creator > Preferences on macOS)
2. Navigate to the "Qode Assist" tab
3. On the "General" page, verify:
- Ollama is selected as your LLM provider
- The URL is set to http://localhost:11434
- Your installed model appears in the model selection
- The prompt template is Ollama Auto FIM or Ollama Auto Chat for chat assistance. You can specify template if it is not work correct
4. Click Apply if you made any changes
You're all set! QodeAssist is now ready to use in Qt Creator.
<details>
<summary>Example of Ollama settings: (click to expand)</summary>
<img width="824" alt="Ollama Settings" src="https://github.com/user-attachments/assets/ed64e03a-a923-467a-aa44-4f790e315b53" />
<summary><strong>Chat Assistant</strong> - Interactive coding assistant (click to expand)</summary>
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CHAT ASSISTANT │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Chat Assistant Settings) │
│ 2. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/chat/*.md │
│ 3. File Context (optional): │
│ ├─ Attached files (manual) │
│ ├─ Linked files (persistent) │
│ └─ Open editor files (if auto-sync enabled) │
│ 4. Tool Definitions (if enabled): │
│ ├─ ReadProjectFileByName │
│ ├─ ListProjectFiles │
│ ├─ SearchInProject │
│ └─ GetIssuesList │
│ 5. Conversation History │
│ 6. User Message │
│ │
│ Final Prompt: [System: SystemPrompt + Rules + Tools] │
│ [History: Previous messages] │
│ [User: FileContext + UserMessage] │
└─────────────────────────────────────────────────────────────────────────────┘
```
</details>
## System Prompt Configuration
<details>
<summary><strong>Quick Refactoring</strong> - Inline code improvements (click to expand)</summary>
The plugin comes with default system prompts optimized for chat and instruct models, as these currently provide better results for code assistance. If you prefer using FIM (Fill-in-Middle) models, you can easily customize the system prompt in the settings.
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUICK REFACTORING │
├─────────────────────────────────────────────────────────────────────────────┤
│ 1. System Prompt (from Quick Refactor Settings) │
│ 2. Project Rules: │
│ ├─ .qodeassist/rules/common/*.md │
│ └─ .qodeassist/rules/quickrefactor/*.md │
│ 3. Code Context: │
│ ├─ File information (language, path) │
│ ├─ Code before selection (configurable amount) │
│ ├─ <selection_start> marker │
│ ├─ Selected code (or current line) │
│ ├─ <selection_end> marker │
│ ├─ <cursor> marker (position within selection) │
│ └─ Code after selection (configurable amount) │
│ 4. Refactor Instruction: │
│ ├─ Built-in (e.g., "Improve Code", "Alternative Solution") │
│ ├─ Custom Instruction (from library) │
│ │ └─ ~/.config/QtProject/qtcreator/qodeassist/ │
│ │ quick_refactor/instructions/*.json │
│ └─ Additional Details (optional user input) │
│ 5. Tool Definitions (if enabled) │
│ │
│ Final Prompt: [System: SystemPrompt + Rules] │
│ [User: Context + Markers + Instruction + Details] │
└─────────────────────────────────────────────────────────────────────────────┘
```
## File Context Features
</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.
### Key Points
### Attached Files
- **Project Rules** are automatically loaded from `.qodeassist/rules/` directory structure
- **System Prompts** are configured independently for each feature in Settings
- **FIM vs Non-FIM models** for code completion use different System Prompts:
- FIM models: Direct completion prompt
- Non-FIM models: Prompt includes response formatting instructions
- **Quick Refactor** has its own provider/model configuration, independent from Chat
- **Custom Instructions** provide reusable templates that can be augmented with specific details
- **Tool Calling** is available for Chat and Quick Refactor when enabled
Attachments are designed for one-time code analysis and specific queries:
- Files are included only in the current message
- Content is discarded after the message is processed
- Ideal for:
- Getting specific feedback on code changes
- Code review requests
- Analyzing isolated code segments
- Quick implementation questions
- Files can be attached using the paperclip icon in the chat interface
- Multiple files can be attached to a single message
### Linked Files
Linked files provide persistent context throughout the conversation:
- Files remain accessible for the entire chat session
- Content is included in every message exchange
- Files are automatically refreshed - always using latest content from disk
- Perfect for:
- Long-term refactoring discussions
- Complex architectural changes
- Multi-file implementations
- Maintaining context across related questions
- Can be managed using the link icon in the chat interface
- Supports automatic syncing with open editor files (can be enabled in settings)
- Files can be added/removed at any time during the conversation
## Template-Model Compatibility
| Template | Compatible Models | Purpose |
|----------|------------------|----------|
| CodeLlama FIM | `codellama:code` | Code completion |
| DeepSeekCoder FIM | `deepseek-coder-v2`, `deepseek-v2.5` | Code completion |
| Ollama Auto FIM | `Any Ollama base/fim models` | Code completion |
| Qwen FIM | `Qwen 2.5 models(exclude instruct)` | Code completion |
| StarCoder2 FIM | `starcoder2 base model` | Code completion |
| Alpaca | `starcoder2:instruct` | Chat assistance |
| Basic Chat| `Messages without tokens` | Chat assistance |
| ChatML | `Qwen 2.5 models(exclude base models)` | Chat assistance |
| Llama2 | `llama2 model family`, `codellama:instruct` | Chat assistance |
| Llama3 | `llama3 model family` | Chat assistance |
| Ollama Auto Chat | `Any Ollama chat/instruct models` | Chat assistance |
See [Project Rules Documentation](docs/project-rules.md) and [Quick Refactoring Guide](docs/quick-refactoring.md) for more details.
## QtCreator Version Compatibility
- QtCreator 15.0.1 - 0.4.8 - 0.4.x
- QtCreator 15.0.0 - 0.4.0 - 0.4.7
- QtCreator 14.0.2 - 0.2.3 - 0.3.x
- QtCreator 14.0.1 - 0.2.2 plugin version and below
## Development Progress
- [x] Basic plugin with code autocomplete functionality
- [x] Improve and automate settings
- [x] Add chat functionality
- [x] Sharing diff with model
- [ ] Sharing project source with model
- [ ] Support for more providers and models
| Qt Creator Version | QodeAssist Version |
|-------------------|-------------------|
| 17.0.0+ | 0.6.0 - 0.x.x |
| 16.0.2 | 0.5.13 - 0.x.x |
| 16.0.1 | 0.5.7 - 0.5.13 |
| 16.0.0 | 0.5.2 - 0.5.6 |
| 15.0.1 | 0.4.8 - 0.5.1 |
| 15.0.0 | 0.4.0 - 0.4.7 |
| 14.0.2 | 0.2.3 - 0.3.x |
| 14.0.1 | ≤ 0.2.2 |
## Hotkeys
- To call manual request to suggestion, you can use or change it in settings
- on Mac: Option + Command + Q
- on Windows: Ctrl + Alt + Q
- To insert the full suggestion, you can use the TAB key
- To insert word of suggistion, you can use Alt + Right Arrow for Win/Lin, or Option + Right Arrow for Mac
All hotkeys can be customized in Qt Creator Settings. Default hotkeys:
| Action | macOS | Windows/Linux |
|--------|-------|--------------|
| Open chat window | ⌥⌘W | Ctrl+Alt+W |
| Close chat window | ⌥⌘S | Ctrl+Alt+S |
| Manual code suggestion | ⌥⌘Q | Ctrl+Alt+Q |
| Accept full suggestion | Tab | Tab |
| Accept word | ⌥→ | Alt+→ |
| Quick refactor | ⌥⌘R | Ctrl+Alt+R |
## Troubleshooting
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 LM Studio, the default is usually http://localhost:1234
For additional support, join our [Discord Community](https://discord.gg/BGMkUsXUgf) or check [GitHub Issues](https://github.com/Palm1r/QodeAssist/issues).
2. Check the endpoint:
## Development Progress
Make sure the endpoint in the settings matches the one required by your provider
- For Ollama, it should be /api/generate
- For LM Studio and OpenAI compatible providers, it's usually /v1/chat/completions
3. Confirm that the selected model and template are compatible:
Ensure you've chosen the correct model in the "Select Models" option
Verify that the selected prompt template matches the model you're using
If you're still experiencing issues with QodeAssist, you can try resetting the settings to their default values:
1. Open Qt Creator settings
2. Navigate to the "Qode Assist" tab
3. Pick settings page for reset
4. Click on the "Reset Page to Defaults" button
- The API key will not reset
- Select model after reset
- [x] Code completion functionality
- [x] Chat assistant with multiple panels
- [x] Diff sharing with models
- [x] Tools/function calling support
- [x] Project-specific rules
- [ ] Full project source sharing
- [ ] Additional provider support
- [ ] MCP (Model Context Protocol) support
## Support the development of QodeAssist
If you find QodeAssist helpful, there are several ways you can support the project:
@ -277,20 +400,43 @@ Every contribution, no matter how small, is greatly appreciated and helps keep t
## 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>
cmake --build .
### Build Steps
where `<path_to_qtcreator>` is the relative or absolute path to a Qt Creator build directory, or to a
combined binary and development package (Windows / Linux), or to the `Qt Creator.app/Contents/Resources/`
directory of a combined binary and development package (macOS), and `<path_to_plugin_source>` is the
relative or absolute path to this plugin directory.
1. Create a build directory:
```bash
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
QML code style: Preferably follow the following guidelines https://github.com/Furkanzmc/QML-Coding-Guide, thank you @Furkanzmc for collect them
C++ code style: check use .clang-fortmat in project
### Code Style
- **QML**: Follow [QML Coding Guide](https://github.com/Furkanzmc/QML-Coding-Guide) by @Furkanzmc
- **C++**: Use `.clang-format` configuration in the project root
- Run formatting before submitting PRs
### Development Guidelines
For detailed development guidelines, architecture patterns, and best practices, see the [project workspace rules](.cursor/rules.mdc).
![qodeassist-icon](https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d)
![qodeassist-icon-small](https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41)

202
RefactorSuggestion.cpp Normal file
View File

@ -0,0 +1,202 @@
/*
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RefactorSuggestion.hpp"
#include "LLMSuggestion.hpp"
#include <QTextBlock>
#include <QTextCursor>
#include <QTextDocument>
#include <texteditor/texteditor.h>
#include <logger/Logger.hpp>
namespace QodeAssist {
namespace {
QString extractLeadingWhitespace(const QString &text)
{
QString indent;
int firstLineEnd = text.indexOf('\n');
QString firstLine = (firstLineEnd != -1) ? text.left(firstLineEnd) : text;
for (int i = 0; i < firstLine.length(); ++i) {
if (firstLine[i].isSpace()) {
indent += firstLine[i];
} else {
break;
}
}
return indent;
}
} // anonymous namespace
RefactorSuggestion::RefactorSuggestion(const Data &suggestion, QTextDocument *sourceDocument)
: TextEditor::TextSuggestion([&suggestion, sourceDocument]() {
Data expandedData = suggestion;
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(0, endPos, sourceDocument->characterCount());
if (startPos != endPos) {
QTextCursor startCursor(sourceDocument);
startCursor.setPosition(startPos);
int startPosInBlock = startCursor.positionInBlock();
if (startPosInBlock > 0) {
startCursor.movePosition(QTextCursor::StartOfBlock);
}
QTextCursor endCursor(sourceDocument);
endCursor.setPosition(endPos);
int endPosInBlock = endCursor.positionInBlock();
if (endPosInBlock > 0) {
endCursor.movePosition(QTextCursor::EndOfBlock);
if (!endCursor.atEnd()) {
endCursor.movePosition(QTextCursor::NextCharacter);
}
}
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
sourceDocument, startCursor.position());
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
sourceDocument, endCursor.position());
expandedData.range = Utils::Text::Range(expandedBegin, expandedEnd);
}
return expandedData;
}(), sourceDocument)
, m_suggestionData(suggestion)
{
const QString refactoredText = suggestion.text;
int startPos = suggestion.range.begin.toPositionInDocument(sourceDocument);
int endPos = suggestion.range.end.toPositionInDocument(sourceDocument);
startPos = qBound(0, startPos, sourceDocument->characterCount());
endPos = qBound(0, endPos, sourceDocument->characterCount());
QTextCursor startCursor(sourceDocument);
startCursor.setPosition(startPos);
if (startPos == endPos) {
QTextBlock block = startCursor.block();
QString blockText = block.text();
int startPosInBlock = startCursor.positionInBlock();
QString leftText = blockText.left(startPosInBlock);
QString rightText = blockText.mid(startPosInBlock);
QString displayText = leftText + refactoredText + rightText;
replacementDocument()->setPlainText(displayText);
} else {
QTextCursor fullLinesCursor(sourceDocument);
fullLinesCursor.setPosition(startPos);
fullLinesCursor.movePosition(QTextCursor::StartOfBlock);
int fullLinesStart = fullLinesCursor.position();
fullLinesCursor.setPosition(endPos);
fullLinesCursor.movePosition(QTextCursor::EndOfBlock);
int fullLinesEnd = fullLinesCursor.position();
fullLinesCursor.setPosition(fullLinesStart);
fullLinesCursor.setPosition(fullLinesEnd, QTextCursor::KeepAnchor);
QString fullLinesText = fullLinesCursor.selectedText();
fullLinesText.replace(QChar(0x2029), "\n");
QString oldIndent = extractLeadingWhitespace(fullLinesText);
QString newIndent = extractLeadingWhitespace(refactoredText);
QString displayText = refactoredText;
if (newIndent.length() < oldIndent.length()) {
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
QStringList lines = refactoredText.split('\n');
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
lines[0] = indentDiff + lines[0];
displayText = lines.join('\n');
}
}
replacementDocument()->setPlainText(displayText);
}
}
bool RefactorSuggestion::apply()
{
const QString text = m_suggestionData.text;
const Utils::Text::Range range = m_suggestionData.range;
const QTextCursor startCursor = range.begin.toTextCursor(sourceDocument());
const QTextCursor endCursor = range.end.toTextCursor(sourceDocument());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(sourceDocument());
editCursor.beginEditBlock();
if (startPos == endPos) {
editCursor.setPosition(startPos);
editCursor.insertText(text);
} else {
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
QString selectedText = editCursor.selectedText();
selectedText.replace(QChar(0x2029), "\n");
QString oldIndent = extractLeadingWhitespace(selectedText);
QString newIndent = extractLeadingWhitespace(text);
QString textToInsert = text;
if (newIndent.length() < oldIndent.length()) {
QString indentDiff = oldIndent.left(oldIndent.length() - newIndent.length());
QStringList lines = text.split('\n');
if (!lines.isEmpty() && !lines[0].trimmed().isEmpty()) {
lines[0] = indentDiff + lines[0];
textToInsert = lines.join('\n');
}
}
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
editCursor.insertText(textToInsert);
}
editCursor.endEditBlock();
return true;
}
bool RefactorSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{
Q_UNUSED(widget)
return apply();
}
bool RefactorSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{
Q_UNUSED(widget)
return apply();
}
} // namespace QodeAssist

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
@ -19,22 +19,25 @@
#pragma once
#include <QJsonArray>
#include <texteditor/texteditor.h>
#include <texteditor/textsuggestion.h>
#include "llmcore/PromptTemplate.hpp"
namespace QodeAssist {
namespace QodeAssist::Templates {
class BasicChat : public LLMCore::PromptTemplate
class RefactorSuggestion : public TextEditor::TextSuggestion
{
public:
LLMCore::TemplateType type() const override { return LLMCore::TemplateType::Chat; }
QString name() const override { return "Basic Chat"; }
QString promptTemplate() const override { return {}; }
QStringList stopWords() const override { return QStringList(); }
void prepareRequest(QJsonObject &request, const LLMCore::ContextData &context) const override
{}
QString description() const override { return "chat without tokens"; }
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::Templates
} // namespace QodeAssist

View File

@ -0,0 +1,210 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "RefactorSuggestionHoverHandler.hpp"
#include "RefactorSuggestion.hpp"
#include <QColor>
#include <QHBoxLayout>
#include <QPushButton>
#include <QScopeGuard>
#include <QTextBlock>
#include <QTextCursor>
#include <QWidget>
#include <texteditor/textdocumentlayout.h>
#include <texteditor/texteditor.h>
#include <utils/theme/theme.h>
#include <utils/tooltip/tooltip.h>
#include <logger/Logger.hpp>
namespace QodeAssist {
RefactorSuggestionHoverHandler::RefactorSuggestionHoverHandler()
{
setPriority(Priority_Suggestion);
}
void RefactorSuggestionHoverHandler::setSuggestionRange(const Utils::Text::Range &range)
{
m_suggestionRange = range;
m_hasSuggestion = true;
}
void RefactorSuggestionHoverHandler::clearSuggestionRange()
{
m_hasSuggestion = false;
}
void RefactorSuggestionHoverHandler::identifyMatch(
TextEditor::TextEditorWidget *editorWidget,
int pos,
ReportPriority report)
{
QScopeGuard cleanup([&] { report(Priority_None); });
if (!editorWidget->suggestionVisible()) {
return;
}
QTextCursor cursor(editorWidget->document());
cursor.setPosition(pos);
m_block = cursor.block();
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
TextEditor::TextBlockUserData::suggestion(m_block));
#else
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
if (!userData) {
LOG_MESSAGE("RefactorSuggestionHoverHandler: No user data in block");
return;
}
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
#endif
if (!suggestion) {
return;
}
cleanup.dismiss();
report(Priority_Suggestion);
}
void RefactorSuggestionHoverHandler::operateTooltip(
TextEditor::TextEditorWidget *editorWidget,
const QPoint &point)
{
Q_UNUSED(point)
#if QODEASSIST_QT_CREATOR_VERSION_MAJOR >= 17
auto *suggestion = dynamic_cast<RefactorSuggestion *>(
TextEditor::TextBlockUserData::suggestion(m_block));
#else
auto *userData = TextEditor::TextDocumentLayout::textUserData(m_block);
if (!userData) {
LOG_MESSAGE("RefactorSuggestionHoverHandler::operateTooltip: No user data in block");
return;
}
auto *suggestion = dynamic_cast<RefactorSuggestion *>(userData->suggestion());
#endif
if (!suggestion) {
return;
}
auto *widget = new QWidget();
auto *layout = new QHBoxLayout(widget);
layout->setContentsMargins(4, 3, 4, 3);
layout->setSpacing(6);
const QColor normalBg = Utils::creatorColor(Utils::Theme::BackgroundColorNormal);
const QColor hoverBg = Utils::creatorColor(Utils::Theme::BackgroundColorHover);
const QColor selectedBg = Utils::creatorColor(Utils::Theme::BackgroundColorSelected);
const QColor textColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
const QColor borderColor = Utils::creatorColor(Utils::Theme::SplitterColor);
const QColor successColor = Utils::creatorColor(Utils::Theme::TextColorNormal);
const QColor errorColor = Utils::creatorColor(Utils::Theme::TextColorError);
auto *applyButton = new QPushButton("✓ Apply", widget);
applyButton->setFocusPolicy(Qt::NoFocus);
applyButton->setToolTip("Apply refactoring (Tab)");
applyButton->setCursor(Qt::PointingHandCursor);
applyButton->setStyleSheet(QString(
"QPushButton {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" border-radius: 3px;"
" padding: 4px 12px;"
" font-weight: bold;"
" font-size: 11px;"
" min-width: 60px;"
"}"
"QPushButton:hover {"
" background-color: %4;"
" border-color: %2;"
"}"
"QPushButton:pressed {"
" background-color: %5;"
"}")
.arg(selectedBg.name())
.arg(successColor.name())
.arg(borderColor.name())
.arg(selectedBg.lighter(110).name())
.arg(selectedBg.darker(110).name()));
QObject::connect(applyButton, &QPushButton::clicked, widget, [this]() {
Utils::ToolTip::hide();
if (m_applyCallback) {
m_applyCallback();
}
});
auto *dismissButton = new QPushButton("✕ Dismiss", widget);
dismissButton->setFocusPolicy(Qt::NoFocus);
dismissButton->setToolTip("Dismiss refactoring (Esc)");
dismissButton->setCursor(Qt::PointingHandCursor);
dismissButton->setStyleSheet(QString(
"QPushButton {"
" background-color: %1;"
" color: %2;"
" border: 1px solid %3;"
" border-radius: 3px;"
" padding: 4px 12px;"
" font-size: 11px;"
" min-width: 60px;"
"}"
"QPushButton:hover {"
" background-color: %4;"
" color: %5;"
" border-color: %5;"
"}"
"QPushButton:pressed {"
" background-color: %6;"
"}")
.arg(normalBg.name())
.arg(textColor.name())
.arg(borderColor.name())
.arg(hoverBg.name())
.arg(errorColor.name())
.arg(hoverBg.darker(110).name()));
QObject::connect(dismissButton, &QPushButton::clicked, widget, [this]() {
Utils::ToolTip::hide();
if (m_dismissCallback) {
m_dismissCallback();
}
});
layout->addWidget(applyButton);
layout->addWidget(dismissButton);
const QRect cursorRect = editorWidget->cursorRect(editorWidget->textCursor());
QPoint pos = editorWidget->viewport()->mapToGlobal(cursorRect.topLeft())
- Utils::ToolTip::offsetFromPosition();
pos.ry() -= widget->sizeHint().height();
Utils::ToolTip::show(pos, widget, editorWidget);
}
} // namespace QodeAssist

View File

@ -0,0 +1,72 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include <QTextBlock>
#include <texteditor/basehoverhandler.h>
#include <utils/textutils.h>
namespace TextEditor {
class TextEditorWidget;
}
namespace QodeAssist {
/**
* @brief Hover handler for refactoring suggestions
*
* Shows interactive tooltip with Apply/Dismiss buttons when hovering over
* a refactoring suggestion in the editor.
*/
class RefactorSuggestionHoverHandler : public TextEditor::BaseHoverHandler
{
public:
using ApplyCallback = std::function<void()>;
using DismissCallback = std::function<void()>;
RefactorSuggestionHoverHandler();
void setSuggestionRange(const Utils::Text::Range &range);
void clearSuggestionRange();
bool hasSuggestion() const { return m_hasSuggestion; }
void setApplyCallback(ApplyCallback callback) { m_applyCallback = std::move(callback); }
void setDismissCallback(DismissCallback callback) { m_dismissCallback = std::move(callback); }
protected:
void identifyMatch(TextEditor::TextEditorWidget *editorWidget,
int pos,
ReportPriority report) override;
void operateTooltip(TextEditor::TextEditorWidget *editorWidget,
const QPoint &point) override;
private:
Utils::Text::Range m_suggestionRange;
bool m_hasSuggestion = false;
ApplyCallback m_applyCallback;
DismissCallback m_dismissCallback;
QTextBlock m_block;
};
} // namespace QodeAssist

14
TaskFlow/CMakeLists.txt Normal file
View 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
)

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

View File

@ -0,0 +1,120 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https:
*/
#include "FlowEditor.hpp"
namespace QodeAssist::TaskFlow {
FlowEditor::FlowEditor(QQuickItem *parent)
: QQuickItem(parent)
{}
void FlowEditor::initialize()
{
emit availableTaskTypesChanged();
emit availableFlowsChanged();
m_flowsModel = new FlowsModel(m_flowManager, this);
emit flowsModelChanged();
if (m_flowsModel->rowCount() > 0) {
setCurrentFlowIndex(0);
}
// setCurrentFlowId(m_flowManager->flows().begin().value()->flowId());
m_currentFlow = m_flowManager->getFlow();
emit currentFlowChanged();
}
QString FlowEditor::currentFlowId() const
{
return m_currentFlowId;
}
void FlowEditor::setCurrentFlowId(const QString &newCurrentFlowId)
{
if (m_currentFlowId == newCurrentFlowId)
return;
m_currentFlowId = newCurrentFlowId;
emit currentFlowIdChanged();
}
QStringList FlowEditor::availableTaskTypes() const
{
if (m_flowManager)
return m_flowManager->getAvailableTasksTypes();
else {
return {"No flow manager"};
}
}
QStringList FlowEditor::availableFlows() const
{
if (m_flowManager) {
auto flows = m_flowManager->getAvailableFlows();
return flows.size() > 0 ? flows : QStringList{"No flows"};
} else {
return {"No flow manager"};
}
}
void FlowEditor::setFlowManager(FlowManager *newFlowManager)
{
if (m_flowManager == newFlowManager)
return;
m_flowManager = newFlowManager;
initialize();
}
FlowsModel *FlowEditor::flowsModel() const
{
return m_flowsModel;
}
int FlowEditor::currentFlowIndex() const
{
return m_currentFlowIndex;
}
void FlowEditor::setCurrentFlowIndex(int newCurrentFlowIndex)
{
if (m_currentFlowIndex == newCurrentFlowIndex)
return;
m_currentFlowIndex = newCurrentFlowIndex;
emit currentFlowIndexChanged();
}
Flow *FlowEditor::getFlow(const QString &flowName)
{
return m_flowManager->getFlow(flowName);
}
Flow *FlowEditor::getCurrentFlow()
{
return m_flowManager->getFlow(m_currentFlowId);
}
Flow *FlowEditor::currentFlow() const
{
return m_currentFlow;
}
} // namespace QodeAssist::TaskFlow

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https:
*/
#pragma once
#include <QQuickItem>
#include "FlowsModel.hpp"
#include <FlowManager.hpp>
namespace QodeAssist::TaskFlow {
class FlowEditor : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(
QString currentFlowId READ currentFlowId WRITE setCurrentFlowId NOTIFY currentFlowIdChanged)
Q_PROPERTY(
QStringList availableTaskTypes READ availableTaskTypes NOTIFY availableTaskTypesChanged)
Q_PROPERTY(QStringList availableFlows READ availableFlows NOTIFY availableFlowsChanged)
Q_PROPERTY(FlowsModel *flowsModel READ flowsModel NOTIFY flowsModelChanged)
Q_PROPERTY(int currentFlowIndex READ currentFlowIndex WRITE setCurrentFlowIndex NOTIFY
currentFlowIndexChanged)
Q_PROPERTY(Flow *currentFlow READ currentFlow NOTIFY currentFlowChanged FINAL)
public:
FlowEditor(QQuickItem *parent = nullptr);
void initialize();
QString currentFlowId() const;
void setCurrentFlowId(const QString &newCurrentFlowId);
QStringList availableTaskTypes() const;
QStringList availableFlows() const;
void setFlowManager(FlowManager *newFlowManager);
FlowsModel *flowsModel() const;
int currentFlowIndex() const;
void setCurrentFlowIndex(int newCurrentFlowIndex);
Q_INVOKABLE Flow *getFlow(const QString &flowName);
Q_INVOKABLE Flow *getCurrentFlow();
Flow *currentFlow() const;
signals:
void currentFlowIdChanged();
void availableTaskTypesChanged();
void availableFlowsChanged();
void flowsModelChanged();
void currentFlowIndexChanged();
void currentFlowChanged();
private:
FlowManager *m_flowManager = nullptr;
QString m_currentFlowId;
FlowsModel *m_flowsModel;
int m_currentFlowIndex;
Flow *m_currentFlow = nullptr;
};
} // namespace QodeAssist::TaskFlow

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,98 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#include "GridBackground.hpp"
#include <QPainter>
#include <QPixmap>
#include <QQuickWindow>
#include <QSGSimpleRectNode>
#include <QSGSimpleTextureNode>
namespace QodeAssist::TaskFlow {
GridBackground::GridBackground(QQuickItem *parent)
: QQuickItem(parent)
{
setFlag(QQuickItem::ItemHasContents, true);
}
int GridBackground::gridSize() const
{
return m_gridSize;
}
void GridBackground::setGridSize(int size)
{
if (m_gridSize != size) {
m_gridSize = size;
update();
emit gridSizeChanged();
}
}
QColor GridBackground::gridColor() const
{
return m_gridColor;
}
void GridBackground::setGridColor(const QColor &color)
{
if (m_gridColor != color) {
m_gridColor = color;
update();
emit gridColorChanged();
}
}
QSGNode *GridBackground::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
{
QSGSimpleTextureNode *node = static_cast<QSGSimpleTextureNode *>(oldNode);
if (!node) {
node = new QSGSimpleTextureNode();
}
QPixmap pixmap(width(), height());
pixmap.fill(Qt::transparent);
QPainter painter(&pixmap);
painter.setRenderHint(QPainter::Antialiasing, false);
QPen pen(m_gridColor);
pen.setWidth(1);
painter.setPen(pen);
painter.setOpacity(this->opacity());
for (int x = 0; x < width(); x += m_gridSize) {
painter.drawLine(x, 0, x, height());
}
for (int y = 0; y < height(); y += m_gridSize) {
painter.drawLine(0, y, width(), y);
}
painter.end();
QSGTexture *texture = window()->createTextureFromImage(pixmap.toImage());
node->setTexture(texture);
node->setRect(boundingRect());
return node;
}
} // namespace QodeAssist::TaskFlow

View File

@ -0,0 +1,57 @@
/*
* Copyright (C) 2025 Petr Mironychev
*
* This file is part of QodeAssist.
*
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <QColor>
#include <QPainter>
#include <QQuickItem>
namespace QodeAssist::TaskFlow {
class GridBackground : public QQuickItem
{
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(int gridSize READ gridSize WRITE setGridSize NOTIFY gridSizeChanged)
Q_PROPERTY(QColor gridColor READ gridColor WRITE setGridColor NOTIFY gridColorChanged)
public:
explicit GridBackground(QQuickItem *parent = nullptr);
int gridSize() const;
void setGridSize(int size);
QColor gridColor() const;
void setGridColor(const QColor &color);
signals:
void gridSizeChanged();
void gridColorChanged();
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;
private:
int m_gridSize = 20;
QColor m_gridColor = QColor(128, 128, 128);
};
} // namespace QodeAssist::TaskFlow

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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