Compare commits

..

571 Commits

Author SHA1 Message Date
Petr Mironychev
bbb9c47cbb chore: Update plugin to 0.9.13 version 2026-04-27 07:12:52 +02:00
Petr Mironychev
46aa53e726 feat: Add llama.cpp modellisting and thinking support 2026-04-26 23:06:54 +02:00
Petr Mironychev
4d320bc065 refactor: Remove legacy code (#344) 2026-04-25 18:48:28 +02:00
Petr Mironychev
7b4e08859c feat: Add trying to parse ollama tools result when it's codeblock 2026-04-25 09:25:14 +02:00
Petr Mironychev
d15b46825e feat: Add support QtCreator 19.0.1 (#343) 2026-04-25 09:13:07 +02:00
Petr Mironychev
e0ab5080ea feat: Add llama.cpp api key support 2026-04-24 10:57:38 +02:00
Petr Mironychev
6a8fbe1792 chore: Update LLMQore to 0.4.2 2026-04-23 20:57:07 +02:00
Petr Mironychev
d867a6f0be docs: Add MCP client description 2026-04-23 20:13:32 +02:00
Petr Mironychev
248530c746 chore: Update plugin version to 0.9.12 2026-04-23 19:48:32 +02:00
Petr Mironychev
c73b71f328 feat: Add max continuation tools settings 2026-04-23 19:23:18 +02:00
Petr Mironychev
d2c1e39a2e chore: Update default ollama models 2026-04-23 11:29:07 +02:00
Petr Mironychev
e86e7e103e feat: Improve assemble string after code suggestion 2026-04-23 11:14:46 +02:00
Petr Mironychev
42199024ff refactor: Improvement code completion auto trigger 2026-04-23 10:56:23 +02:00
Petr Mironychev
620fded2e1 feat: Add mcp client hub 2026-04-23 10:18:57 +02:00
Petr Mironychev
90b7ed26b1 docs: Update current features in README.md 2026-04-23 03:42:35 +02:00
Petr Mironychev
25c4d5f185 feat: LM Studio response API and Ollama OpenAI API 2026-04-23 03:35:56 +02:00
Petr Mironychev
7a551ed384 feat: Add qodeassist mcp server 2026-04-23 02:40:46 +02:00
Petr Mironychev
ca0a47b160 feat: Improve Chat UI
Move send and compress button to right bottom corner
2026-04-23 01:48:17 +02:00
Petr Mironychev
6b069b55e3 chore: Update copyrights 2026-04-21 08:57:06 +02:00
Petr Mironychev
2891b313d2 refactor: Separate and simplified tools (#340) 2026-04-19 18:12:15 +02:00
Petr Mironychev
ede2c01eb7 Update LLMQore to v0.0.4 (#339) 2026-04-19 11:58:54 +02:00
Petr Mironychev
6c05f0d594 refactor: Add external LLMCore lib (#334)
* feat: Add LLMCore submodule
2026-04-03 12:30:40 +02:00
Ivan Lebedev
15d714588f fix: Open Qt Creator settings delayed from ChatRootView (#332)
fix: Opens settings delayed

Co-authored-by: Ivan Lebedev <ilebedev1988@gmail.com>
2026-03-29 11:58:34 +02:00
Petr Mironychev
9a2ba08538 chore: Upgrade plugin to 0.9.11 2026-03-13 00:56:25 +01:00
Petr Mironychev
37084bec59 feat: Improve execute terminal command tool 2026-03-13 00:34:20 +01:00
Petr Mironychev
6910037e97 feat: Update models configuration 2026-03-12 23:58:06 +01:00
Petr Mironychev
a72cdd85a4 feat: Add support QtC 19
remove support QtC 17
2026-03-12 23:31:35 +01:00
lebedeviv1988
31b4e73af5 fix: Qt Creator 19 API breaking changes (#328)
* Inherits `QodeAssist::Settings::AgentRolesWidget` from `Core::IOptionsPageWidget`

* Adds `QodeAssist::Settings::showSettings` function and use it instead `Core::ICore::showOptionsDialog`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 16:00:51 +01:00
lebedeviv1988
088887c802 fix: enables the send message shortcut only for active chat (#322)
fix: Disables sending message shortcut instead of filtering in `Shortcut::activated` signal handler

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 12:01:14 +01:00
lebedeviv1988
b7a9787cc3 refactor: Refactors AgentRoleDialog's modes handling (#325)
* fix: Fixes `undefined-bool-conversion` compilation warning.

* refactor: Replaces `AgentRoleDialog::m_editMode` with `AgentRoleDialog::m_action`

---------

Co-authored-by: Ivan Lebedev <ilebedev@flightpath3d.com>
2026-03-05 10:48:01 +01:00
Petr Mironychev
e2e13f0f38 refactor: Improve http client (#319) 2026-02-25 15:13:05 +01:00
Petr Mironychev
49ae335d7d chore: Update plugin to 0.9.10 2026-02-25 12:33:14 +01:00
Petr Mironychev
2ba58a403f refactor: UI for opening content from chat (#318)
* refactor: Changed options to opening images from chat
* refactor: Add customizable tooltip
2026-02-25 07:49:37 +01:00
Petr Mironychev
3de1619bf0 feat: Add file search to chat (#317) 2026-02-22 13:53:44 +01:00
Petr Mironychev
ec45067336 chore: Upgrade plugin to 0.9.9 version 2026-01-27 22:41:57 +01:00
Petr Mironychev
52fb65c5b1 feat: Add support QtCreator 18.0.2 2026-01-27 22:41:20 +01:00
Petr Mironychev
478f369ad2 feat: Add codestral and mistral quick setup 2026-01-27 22:41:02 +01:00
Petr Mironychev
762c965377 fix: Add preconditions for windows chat 2026-01-27 22:35:02 +01:00
Petr Mironychev
d2b93310e2 chore: Update plugin to 0.9.8 2026-01-20 20:00:49 +01:00
Petr Mironychev
f3b1e7f411 Add quick setup screenshot 2026-01-20 19:57:44 +01:00
Petr Mironychev
a55c6ccfdb feat: Add predefined templates 2026-01-20 19:54:16 +01:00
Petr Mironychev
b32433c336 refactor: Change quick refactor ui layout 2026-01-20 18:08:49 +01:00
Petr Mironychev
6f11260cd1 refactor: Change UI for fix behavior 2026-01-19 23:52:44 +01:00
Petr Mironychev
ddd6aba091 fix: Remove close chat action from editor context menu 2026-01-19 23:17:31 +01:00
Dinesh Bala
e3f464c54e fix: Create _content folder only when there is an attachment (#297) 2025-12-16 13:19:10 +01:00
Petr Mironychev
e86e58337a Update QodeAssist version range for Qt Creator 16.0.2 2025-12-15 01:00:00 +01:00
Petr Mironychev
dbd47387be chore: Update plugin to 0.9.7 2025-12-15 00:47:50 +01:00
Petr Mironychev
50e1276ab2 feat: Add support QtC 18.0.1 (#296)
* feat: Add support QtC 18.0.1
* feat: Remove support QtC 16.0.2
2025-12-14 02:53:58 +01:00
Petr Mironychev
50c948ccfe chore: Update plugin to 0.9.6 version 2025-12-08 11:10:20 +01:00
Petr Mironychev
949dad4fd2 feat: Update built in roles and docs 2025-12-08 11:09:21 +01:00
Petr Mironychev
01fd7dad6f chore: Upgrade plugin to 0.9.5 version 2025-12-08 10:25:38 +01:00
Petr Mironychev
fd408ba415 fix: Change compressing icon place 2025-12-08 10:24:27 +01:00
Petr Mironychev
14e7ea2ec3 feat: Add separator to chat top bar 2025-12-08 10:18:12 +01:00
Petr Mironychev
9f050aec67 feat: Add summarize chat (#289) 2025-12-05 11:08:23 +01:00
Petr Mironychev
9e118ddfaf fix: Add pause between call tools 2025-12-04 21:05:13 +01:00
Petr Mironychev
157498b770 revert: Remove compact mode for chat blocks 2025-12-04 20:51:14 +01:00
Petr Mironychev
5c8a8f305d Fix chat scrolling (#288)
* fix: Change chat scrolling behavior
* feat: Add compact mode for chat blocks
2025-12-04 20:00:53 +01:00
Petr Mironychev
fc33bb60d0 feat: Add agent roles (#287)
* feat: Add agent roles
* doc: Add agent roles to docs
2025-12-04 19:41:30 +01:00
Petr Mironychev
498eb4d932 feat: Add todo tool (#286) 2025-12-03 21:28:59 +01:00
Petr Mironychev
fb941cea99 doc: Update docs for using OpenAI GPT-5.1 and codex 2025-12-01 16:08:21 +01:00
Petr Mironychev
a0af983bda chore: Upgrade plugin to 0.9.4 version 2025-12-01 15:50:28 +01:00
Petr Mironychev
4bd96e0718 fix: Add downloading svg (#285) 2025-12-01 14:11:09 +01:00
Petr Mironychev
7b0d3c2abb feat: Add settings shortcut to chat view 2025-12-01 13:39:26 +01:00
Petr Mironychev
75d1551b00 feat: Add settings button to quick refactor dialog 2025-12-01 13:21:50 +01:00
Petr Mironychev
406ba05bfb fix: Remove unusing variables from settings 2025-12-01 13:16:45 +01:00
Petr Mironychev
7a97d0aba5 feat: Add llm config to quick refactor dialog 2025-12-01 13:01:46 +01:00
Petr Mironychev
b19c4c0c0c fix: Check Request id for tool block 2025-12-01 12:31:06 +01:00
Petr Mironychev
a466332822 feat: Add OpenAI Responses API (#282)
* feat: Add OpenAI Responses API

* fix: Make temperature optional

* chore: Increase default value of max tokens
2025-12-01 12:14:55 +01:00
Petr Mironychev
e1fa01d123 fix: check request id of thinking block 2025-12-01 00:58:33 +01:00
Petr Mironychev
37e41d3b76 chore: Upgrade plugin version to 0.9.3 2025-11-28 16:21:28 +01:00
Petr Mironychev
2d5667d8ca refactor: Reworking attaching files (#280)
chore: Upgrade chat format version
2025-11-28 16:17:25 +01:00
Petr Mironychev
22377c8f6a feat: Add temp file storage for chat (#279)
* fix: Add signature to chat history
* feat: Add file storage for chat
2025-11-28 13:59:43 +01:00
Petr Mironychev
595895840a fix: Adapt widget to content size 2025-11-28 13:59:14 +01:00
Petr Mironychev
f6d647d5c8 fix: Add signature to chat history 2025-11-28 12:28:07 +01:00
Petr Mironychev
1f9c60ffb2 doc: Fix description for code completion and quick refactor (#278) 2025-11-27 14:02:27 +01:00
Petr Mironychev
6ec4a61c0c chore: Update plugin to 0.9.2 2025-11-27 02:10:51 +01:00
Petr Mironychev
7feb088de3 fix: Change windows command and checks (#277) 2025-11-27 01:53:41 +01:00
Petr Mironychev
627a821115 fix: Edit file tool take only absolute or relative path to file 2025-11-27 01:53:27 +01:00
Petr Mironychev
9b0ae98f02 feat: Improve execute terminal commands tool 2025-11-27 01:12:21 +01:00
Petr Mironychev
85a7bba90e refactor: Change top bar layout and tools/thinking settings 2025-11-27 00:39:37 +01:00
Petr Mironychev
b18ef4c400 feat: Add build folder path to system prompt 2025-11-26 19:48:54 +01:00
Petr Mironychev
bbacdfc22a feat: Add run option to build project tool 2025-11-26 19:23:56 +01:00
Petr Mironychev
670f81c3dd feat: Add side by side refactor widget (#276) 2025-11-26 15:45:22 +01:00
Petr Mironychev
b4f31dee23 feat: Add ollama thinking feature (#275) 2025-11-26 10:32:04 +01:00
Petr Mironychev
dc6ec4fb4f refactor: Add reserve size for commands 2025-11-25 23:33:52 +01:00
Petr Mironychev
07de415346 feat: Add execution command tool (#273) 2025-11-23 12:52:20 +01:00
Petr Mironychev
a15f64a234 feat: Improve build project tool (#272) 2025-11-22 13:15:15 +01:00
Petr Mironychev
0feaa3a0f7 chore: Upgrade plugin version to 0.9.1 2025-11-20 23:26:06 +01:00
Petr Mironychev
a3527e1442 fix: Remove image dublicate (#271) 2025-11-20 23:25:00 +01:00
Petr Mironychev
24565dc81f chore: Move settings it setups 2025-11-20 18:04:55 +01:00
Petr Mironychev
90655cded4 feat: Add settings for ignore space and tab in codecompletion char count 2025-11-20 17:37:00 +01:00
Petr Mironychev
1e3b1997cc feat: Add configuration manager (#270) 2025-11-20 17:31:13 +01:00
Petr Mironychev
6f7d8a0987 feat: Add drag n drop for chat (#269)
feat: Add dran n drop for chat
2025-11-20 16:19:50 +01:00
Petr Mironychev
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
Petr Mironychev
ce9e2717d6 refactor: Change default code completion type to auto 2025-11-19 01:19:50 +01:00
Petr Mironychev
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
Petr Mironychev
bcdec96d92 fear: Add hint-trigger for call code completion (#266) 2025-11-17 22:24:04 +01:00
Petr Mironychev
86c6930c5f chore: Update plugin to 0.9.0 2025-11-17 15:56:00 +01:00
Petr Mironychev
f3aa706227 feat: Add default option for custom instructions 2025-11-17 15:54:35 +01:00
Petr Mironychev
944e7fb00a fix: Set default focus on custom commands in quick refactor dialog 2025-11-17 15:44:55 +01:00
Petr Mironychev
296a0ff7b8 fix: Chagne progress widget icon (#259)
fix: add manual icon
2025-11-17 15:19:25 +01:00
Petr Mironychev
06bd7db7ea chore: Remove extra comments 2025-11-17 14:39:59 +01:00
Petr Mironychev
43f9e4e75b doc: Add context layers to README 2025-11-17 14:17:10 +01:00
Petr Mironychev
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
Petr Mironychev
995597d789 fix: Filter empty thinking blocks for google ai 2025-11-17 11:35:38 +01:00
Petr Mironychev
9974b2f5e6 feat: Run for quick refactoring only reading context tools 2025-11-17 11:25:42 +01:00
Petr Mironychev
31e3d9db7c feat: Add cancel function for progress indicator 2025-11-16 16:14:12 +01:00
Petr Mironychev
6f680e3974 fix: Remove extra logs 2025-11-15 14:55:22 +01:00
Petr Mironychev
953774aaa8 refactor: Full rework quick refactor (#257) 2025-11-15 14:51:47 +01:00
Petr Mironychev
9ecd285d1d feat: Improve code suggestions (#256) 2025-11-14 17:02:43 +01:00
Petr Mironychev
0ca1decd97 fix: Update plugin to 0.8.3 2025-11-14 02:11:15 +01:00
Petr Mironychev
baf129f0dc feat: Add download button for updater 2025-11-14 02:00:33 +01:00
Petr Mironychev
8570b9667a refactor: Change handling finished tools and thinking blocks 2025-11-14 01:46:36 +01:00
Petr Mironychev
f5a445b021 feat: Add updater to release folder 2025-11-14 00:30:38 +01:00
Petr Mironychev
30885c0373 feat: Add google provider thinking mode (#255)
fix: add signature
2025-11-13 23:52:38 +01:00
Petr Mironychev
5e580b8792 chore: Update plugin to 0.8.2 2025-11-13 02:31:05 +01:00
Petr Mironychev
5352fd4f0b doc: Add thinking mode to README 2025-11-13 02:28:38 +01:00
Petr Mironychev
6e9db1552c fix: Replace text to icon in Reset button 2025-11-13 02:23:45 +01:00
Petr Mironychev
c302138568 feat: Add disappearing tools and thinking component from chat 2025-11-13 02:19:49 +01:00
Petr Mironychev
75cbc46808 fix: Add write settings after changing 2025-11-13 01:20:50 +01:00
Petr Mironychev
1070de6e6e fix: Improve edit file tool description 2025-11-13 01:14:14 +01:00
Petr Mironychev
b3432cd76f fix: Clean resources after finish request 2025-11-13 00:56:41 +01:00
Petr Mironychev
f99e4aefb0 refactor: Change opening url in chat by mouse 2025-11-13 00:56:13 +01:00
Petr Mironychev
191de10926 refactor: Remove common project rules from code completion 2025-11-13 00:55:20 +01:00
Petr Mironychev
8492ef29b2 fix: Text color in rules viewer 2025-11-13 00:40:10 +01:00
Petr Mironychev
fea9ecddc8 fix: Remove replace message after complete receiving 2025-11-12 20:32:01 +01:00
Petr Mironychev
a26d475806 fix: Add active project name and path to system prompt 2025-11-12 19:31:12 +01:00
Petr Mironychev
1cd19aa5d1 feat: Add open link to text block context menu 2025-11-12 19:18:21 +01:00
Petr Mironychev
4956d6ab7d feat: Add context menu to edit item 2025-11-12 19:06:29 +01:00
Petr Mironychev
2d92b8fa53 fix: Change params for gpt-5 and o- models
only for chat\completions models
2025-11-12 18:41:51 +01:00
Petr Mironychev
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
Petr Mironychev
89797639cf fix: Getting http errors (#252) 2025-11-11 09:00:33 +01:00
Petr Mironychev
4d79760481 doc: Remove qtc badges 2025-11-11 00:45:12 +01:00
Petr Mironychev
3be70556ec doc: Move out docs from main README 2025-11-10 21:16:58 +01:00
Petr Mironychev
a1ff17eef0 doc: Add info about QodeAssistUpdater 2025-11-10 20:52:45 +01:00
Petr Mironychev
6c1d9ddc0e Upgrade plugin to 0.8.1 2025-11-03 18:07:29 +01:00
Petr Mironychev
6ff6901421 fix: Remove check empty file for edit tool 2025-11-03 18:06:51 +01:00
Petr Mironychev
a2f3ae4f64 fix: Changes to top of file 2025-11-03 15:01:51 +01:00
Petr Mironychev
6937b48fbf fix: Creating dir for new file 2025-11-03 13:56:25 +01:00
Petr Mironychev
c6e77c59d3 refactor: remove hardcoded tools guildelines 2025-11-03 10:07:47 +01:00
Petr Mironychev
655471aec6 chore: Upgrade plugin to 0.8.0 2025-11-03 09:14:47 +01:00
Petr Mironychev
af90d3cad2 refactor: Move build tools to experimental tools 2025-11-03 09:01:20 +01:00
Petr Mironychev
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
Petr Mironychev
e7110810f8 fix: Clear connection before cancel 2025-11-01 20:51:01 +01:00
Petr Mironychev
1848d44503 fix: Chat mode default value 2025-10-31 23:23:08 +01:00
Petr Mironychev
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
Petr Mironychev
9117572f82 feat: Add Text slider button 2025-10-31 09:52:22 +01:00
Petr Mironychev
a143cc8e20 fix: Add ctrl+enter shortcut 2025-10-31 09:13:40 +01:00
Petr Mironychev
eae2b748d5 fix: Chat items width 2025-10-31 09:05:17 +01:00
Petr Mironychev
64bca47290 feat: Add support QtCreator 18 (#246) 2025-10-31 04:08:37 +01:00
Petr Mironychev
531fce96b5 fix: Build QString() compilation error 2025-10-29 01:15:50 +01:00
Petr Mironychev
e7e437590a fix: Build error and tool guideline 2025-10-29 01:09:14 +01:00
Petr Mironychev
00b7287e08 refactor: Back use tools by default and disabling auto apply 2025-10-29 01:01:51 +01:00
Petr Mironychev
5a49a2e7eb refactor: Optimize searching tools
refactor: Merge read and find tool
2025-10-29 00:56:53 +01:00
Petr Mironychev
3b56c1f07a feat: Add build tool 2025-10-28 23:50:44 +01:00
Petr Mironychev
d483ca372d revert: Remove edit file tool (#245) 2025-10-28 16:39:49 +01:00
Petr Mironychev
dfd9979450 refactor: Optimize tool guidelines and disable tools by default 2025-10-26 21:04:37 +01:00
Petr Mironychev
6cb0b14b18 feat: Add log to search tool 2025-10-26 11:57:37 +01:00
Petr Mironychev
43b64b9166 refactor: Simplified edit tool (#242)
refactor: Re-work edit file tool
2025-10-26 11:47:16 +01:00
Petr Mironychev
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
Petr Mironychev
e1025df21e feat: Add tool for creating file 2025-10-23 16:32:40 +02:00
Petr Mironychev
fad2453dbe fix: Remove using tools from QuickRefactoring feature 2025-10-23 16:16:47 +02:00
Petr Mironychev
5e1530715c fix: QString() issue in linux build 2025-10-23 16:14:28 +02:00
Petr Mironychev
dfac209c23 feat: Add multiply reading to read files tool 2025-10-23 15:47:10 +02:00
Petr Mironychev
cab8718979 refactor: Find symbol tool return only url and line 2025-10-23 15:07:22 +02:00
Petr Mironychev
5dc28fc1ad refactor: Improve tool guidelines 2025-10-23 15:06:15 +02:00
Petr Mironychev
1122332423 fix: Selection for code changes 2025-10-22 09:19:32 +02:00
Petr Mironychev
7e878cdbf8 fix: Add tool exception for logging purpose 2025-10-21 00:51:42 +02:00
Petr Mironychev
c95b20d6d4 fix: Override file edit tool another assistant message 2025-10-21 00:27:47 +02:00
Petr Mironychev
70c610997a feat: Add settings for read files only from current project 2025-10-21 00:21:53 +02:00
Petr Mironychev
8a4bf54fff feat: Improve tools guidelines 2025-10-20 19:25:21 +02:00
Petr Mironychev
db7da29fa4 fix: Missed QString linux compilation error 2025-10-20 18:52:01 +02:00
Petr Mironychev
a0a76f2665 fix: QString compilation 2025-10-20 18:45:23 +02:00
Petr Mironychev
56354e8d87 feat: Add find file tool 2025-10-20 18:38:12 +02:00
Petr Mironychev
b7322be00c fix: Read file by path description 2025-10-20 18:23:18 +02:00
Petr Mironychev
254fac246d feat: Add settings for auto apply changes 2025-10-20 18:10:21 +02:00
Petr Mironychev
0365018834 fix: Change parameter for read file tool 2025-10-20 16:17:45 +02:00
Petr Mironychev
755be518be fix: Empty context for empty file 2025-10-20 13:52:13 +02:00
Petr Mironychev
fe82b48bef feat: Add find symbol tool
* improve other tools for reading context
2025-10-20 12:32:03 +02:00
Petr Mironychev
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
Petr Mironychev
238ca00227 Update README.md 2025-10-15 13:28:02 +02:00
Petr Mironychev
18fb2b530f chore: Upgrade plugin to 0.7.1 2025-10-14 02:35:52 +02:00
Petr Mironychev
f0d2e42680 feat: Add support QtCreator 17.0.2 (#239)
feat: Add support QtC 17.0.2
2025-10-14 02:19:14 +02:00
Petr Mironychev
ff0f994ec6 feat: Add project-specific rules support 2025-10-14 01:53:44 +02:00
Petr Mironychev
45df27e749 feat: Add tool for reading issues tab 2025-10-13 18:33:17 +02:00
Petr Mironychev
02863003a9 doc: Added tools info to README.md 2025-10-12 13:29:47 +02:00
Petr Mironychev
002b8e01e5 chore: Upgrade plugin to 0.7.0 2025-10-12 12:28:24 +02:00
Petr Mironychev
f54d1185aa feat: Improve context menu for tool results in chat 2025-10-12 12:16:37 +02:00
Petr Mironychev
bcb0c6f761 fix: Copy selected in code block instead all 2025-10-12 12:03:12 +02:00
Petr Mironychev
d285ab6117 fix: Improve tool handler tools execution 2025-10-12 11:18:20 +02:00
Petr Mironychev
5ae6f9e3bf feat: Add searching tool 2025-10-12 04:25:56 +02:00
Petr Mironychev
fb5903e44f fix: Remove unnecessary log 2025-10-12 03:57:40 +02:00
Petr Mironychev
ce66c8e4f7 feat: Add tools permissions (#238) 2025-10-12 03:56:05 +02:00
Petr Mironychev
5f094887e7 refactor: remove navigation panel 2025-10-12 02:33:21 +02:00
Petr Mironychev
69d9af1a97 feat: Add tooling support to google provider (#237) 2025-10-11 19:46:27 +02:00
Petr Mironychev
86b52bf858 feat: Add context menu to input field and text blocks 2025-10-11 18:35:19 +02:00
Petr Mironychev
cac6068ee7 refactor: Fix copy button and add context menu to code block 2025-10-11 18:23:02 +02:00
Petr Mironychev
8d495dd1bf feat: Add navigation panel for messages 2025-10-11 18:02:08 +02:00
Petr Mironychev
906c161729 feat: Add ollama support tooling (#236) 2025-10-11 10:42:31 +02:00
Petr Mironychev
ebd71daf3d fix: Cleanup accumulated text in one request 2025-10-10 16:45:23 +02:00
Petr Mironychev
84770abb20 fix: Remove duplicate enum 2025-10-10 13:17:16 +02:00
Petr Mironychev
b4e8bdf6da fix: Handling request error on provider error 2025-10-10 10:53:06 +02:00
Petr Mironychev
d2b28093a6 feat: Improve showing tools in chat (#235) 2025-10-10 10:03:22 +02:00
Petr Mironychev
bde58fb9aa Update FUNDING.yml 2025-10-02 13:58:05 +02:00
Petr Mironychev
d4b6f8976b Update FUNDING.yml 2025-10-02 13:56:30 +02:00
Petr Mironychev
cd08b5d919 feat: Add OpenAI compatible providers tooling support (#234)
* remove old providers message handler
2025-10-01 17:23:05 +02:00
Petr Mironychev
f6de03f601 feat: Add llama.cpp tooling support 2025-10-01 16:37:44 +02:00
Petr Mironychev
1a08eebe92 feat: Add Mistral AI tooling support 2025-10-01 15:58:45 +02:00
Petr Mironychev
ea4f8b9df9 feat: Freeze commit hash for gh actions (#233) 2025-10-01 15:32:41 +02:00
Petr Mironychev
7f77f7175d feat: Add tooling support for LM Studio 2025-10-01 12:33:12 +02:00
Petr Mironychev
bed42f9098 feat: Add OpenAI tooling support (#232) 2025-10-01 00:58:54 +02:00
Petr Mironychev
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
Petr Mironychev
8aa37c5c8c refactor: Move Message type enum to separate header 2025-09-30 19:37:46 +02:00
Petr Mironychev
f8b87da2ca refactor: Remove inja submodule 2025-09-30 19:25:34 +02:00
Petr Mironychev
7663bd34af refactor: Move UI controls to own lib 2025-09-29 18:55:51 +02:00
Petr Mironychev
a52c86c6f0 fix: remove Cmake dev variable 2025-09-29 17:54:35 +02:00
Petr Mironychev
ac53296e85 fix: Change behavior of cancel request
*now cancel request cancel all requests
2025-09-28 15:28:01 +02:00
Petr Mironychev
c688cba3dd fix: Change handling shortcuts for handling copy-paste 2025-09-28 15:12:55 +02:00
Petr Mironychev
ff750c271a feat: Add list project files tool 2025-09-23 00:09:56 +02:00
Petr Mironychev
d0f8c1098f feat: Add tools, fabric and executable handler (#230) 2025-09-22 12:36:13 +02:00
Petr Mironychev
5cde6ffac7 feat: Add tool interface and factory 2025-09-17 22:24:31 +02:00
Petr Mironychev
8c6f1e514b fix: Fully qualified for Provider signals and slots 2025-09-17 20:29:53 +02:00
Petr Mironychev
99cd79aac8 refactor: Add request id as type 2025-09-17 20:27:49 +02:00
Petr Mironychev
d2b6c11569 fix: Reset for char renderer 2025-09-17 19:43:20 +02:00
Petr Mironychev
ec1b5bdf5f refactor: Remove non-streaming support (#229) 2025-09-17 19:38:27 +02:00
Petr Mironychev
561661b476 fix: Compatibility problem with nvenc on windows
* change default chat render to software on windows
2025-09-17 10:20:19 +02:00
Petr Mironychev
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
Petr Mironychev
5969d530bd chore: Upgrade plugin to 0.6.2 2025-09-01 00:51:03 +02:00
Petr Mironychev
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
Petr Mironychev
851e681cf5 feat: Add new http client 2025-08-30 18:34:10 +02:00
Petr Mironychev
f2f3b7cce0 doc: Add hotkey to close chat view 2025-08-20 09:31:27 +02:00
Petr Mironychev
5b7a9b681c doc: Update chat description in README.md 2025-08-19 11:41:20 +02:00
Petr Mironychev
29af277139 doc: Added chat and hotkeys description 2025-08-18 12:29:03 +02:00
Petr Mironychev
a45786bd00 chore: Upgrade plugin to 0.6.1 2025-08-18 12:07:27 +02:00
Petr Mironychev
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
Petr Mironychev
5a23ab9c5a fix: Add compatible version in json info 2025-08-18 10:12:57 +02:00
Petr Mironychev
c36dffea93 refactor: Add model output settings instead smartprocessing setting (#220) 2025-08-17 22:01:26 +02:00
Petr Mironychev
4b7eed2779 fix: Change icon for close chat 2025-08-16 23:36:23 +02:00
Petr Mironychev
88c11c4702 fix: Change status bar icon for show chat 2025-08-16 23:21:29 +02:00
Petr Mironychev
543c79161d fix: Clean connection for workaround http2 windows problem (#219) 2025-08-15 10:17:40 +02:00
Petr Mironychev
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
Petr Mironychev
894fec860a fix: Change LMStudio completion endpoint 2025-08-08 16:38:02 +02:00
Petr Mironychev
e4324f8e80 refactor: Remove checking format on CI 2025-08-08 15:23:38 +02:00
Petr Mironychev
6a0198ae9b refactor: Moved execute function to protected 2025-07-20 12:52:02 +02:00
Petr Mironychev
e136d6056a feat: Add basic task flow run and edit (#212) 2025-07-12 23:44:34 +02:00
Petr Mironychev
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
Petr Mironychev
0bdf77f38d feat: Add possibility for translation 2025-07-04 00:40:05 +02:00
Petr Mironychev
21814e8809 fix: Change qtc version typo in README.md 2025-06-24 14:22:30 +02:00
Petr Mironychev
d732e2f9aa chore: Upgrade plugin version to 0.6.0 2025-06-18 20:04:36 +02:00
Petr Mironychev
bf6d09a068 fix: Apply part of code suggestion (#203) 2025-06-18 20:00:18 +02:00
Petr Mironychev
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
Petr Mironychev
af3fdb58ff fix: Add custom endpoint to reset function 2025-05-18 20:06:46 +02:00
Petr Mironychev
637a4d9d4c feat: Add custom providers endpoint (#188) 2025-05-17 09:21:06 +02:00
Petr Mironychev
7e2345773f doc: Add support QtC 16.0.2 to README.md 2025-05-14 19:20:35 +02:00
Petr Mironychev
14a5ddbdd8 chore: Upgrade plugin to 0.5.13 2025-05-14 19:09:16 +02:00
Petr Mironychev
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
Petr Mironychev
4b353d5091 doc: Update targets in README.md 2025-05-10 15:09:35 +02:00
Petr Mironychev
f7ba7b95be doc: Fix ignore files api in README.md 2025-05-02 08:58:31 +02:00
Petr Mironychev
6ae95fec45 doc: Added quick refactoring feature description to README.md 2025-05-02 08:31:34 +02:00
Petr Mironychev
dad8ab2bf3 chore: Update plugin to 0.5.12 2025-05-01 15:38:05 +02:00
Petr Mironychev
25a6983de0 refactor: Make connection more async (#182) 2025-05-01 15:35:33 +02:00
Petr Mironychev
4e05abc7d2 feat: Add settings for text format 2025-05-01 00:01:44 +02:00
Petr Mironychev
784529e344 feat: Add chat font settings (#180) 2025-04-30 22:44:59 +02:00
Petr Mironychev
155153a763 fix: Optimize searching unreadable symbols for markdown 2025-04-30 21:23:43 +02:00
Petr Mironychev
9225c0c1a9 fix: Check readable symbols for markdown 2025-04-29 21:55:44 +02:00
Petr Mironychev
43adc95857 feat: Add a floating "copy" button 2025-04-28 09:25:39 +02:00
Petr Mironychev
ee672f2cda refactor: Remove Chat preview scrollbar 2025-04-26 17:29:06 +02:00
Petr Mironychev
a3edb8a577 chore: Upgrade plugin to 0.5.11 2025-04-24 21:46:32 +02:00
Petr Mironychev
407d3b11c0 fix: Change maximum limit of chat tokens 2025-04-24 21:44:49 +02:00
Petr Mironychev
285e739074 refactor: Change base text style render to markdown 2025-04-24 21:38:54 +02:00
Petr Mironychev
f7e748ba7e chore: Upgrade plugin to 0.5.10 2025-04-24 03:24:00 +02:00
Petr Mironychev
acb1306321 fix: Improve detect unclose codeblock 2025-04-24 03:21:19 +02:00
Petr Mironychev
8b38ecc29b feat: Add Chat preview scroll bar 2025-04-24 02:54:21 +02:00
Petr Mironychev
cfb364f033 fix: Correct removing latest item in messages list 2025-04-24 01:32:01 +02:00
Petr Mironychev
2fe6850a06 refactor: Improve textsuggestion working 2025-04-24 01:25:45 +02:00
Petr Mironychev
3e9506ca92 doc: Add description of ignoring files feature to README.md 2025-04-21 09:40:34 +02:00
Petr Mironychev
d24adff0f5 chore: Update plugin version to 0.5.9 2025-04-21 09:18:02 +02:00
Petr Mironychev
447324eb07 feat: Make artifacts name more meaningful (#172) 2025-04-21 09:17:16 +02:00
Petr Mironychev
4ca494cc51 fix: Suggestion symbols count 2025-04-21 09:02:02 +02:00
Petr Mironychev
8a80dbe8f5 fix: Exclude ignore file from attach 2025-04-21 08:37:57 +02:00
Petr Mironychev
2b539bbdeb fix: Remove check ignoring file on open 2025-04-21 08:08:20 +02:00
Petr Mironychev
3f2c146df1 fix: Save codestral api key 2025-04-20 09:57:14 +02:00
Petr Mironychev
9a54f04a0d feat: Add Codestral as separate ai provider (#171) 2025-04-20 09:48:36 +02:00
Petr Mironychev
7a33425d1a feat: Add reset button to clean message list to specific message (#168) 2025-04-18 19:06:17 +02:00
Petr Mironychev
711aa672f2 fix: Increase mac threshold tokens for tokens (#167) 2025-04-18 17:56:48 +02:00
Petr Mironychev
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
Petr Mironychev
2f9622e23e Update details for Quick Refactor tool in README.md 2025-04-18 08:18:46 +02:00
Petr Mironychev
674b1fecde chore: Upgrade plugin version to 0.5.8 2025-04-17 10:40:10 +02:00
Petr Mironychev
b36d01d2c7 feat: Improve quick refactor dialog (#165) 2025-04-17 10:34:31 +02:00
Petr Mironychev
615175bea8 feat: Add file list for ignoring in request for llm (#163) 2025-04-17 09:12:47 +02:00
Petr Mironychev
7515599acb doc: Add hotkey description for Quick Refactor to README.md 2025-04-14 14:38:12 +02:00
Petr Mironychev
3652d4d5d9 Fix QtCreator version compatibility 2025-04-14 10:54:01 +02:00
Petr Mironychev
75677770b2 Update QtCreator version compatibility README.md 2025-04-14 10:52:45 +02:00
Petr Mironychev
329a1efd5d Update QtCreator version in README.md 2025-04-14 10:51:51 +02:00
Petr Mironychev
27760a3b99 doc: Add example of Quick Refactor to README.md 2025-04-14 01:59:49 +02:00
Petr Mironychev
a93b3cd7f5 chore: Upgrade plugin to QtCreator 16.0.1 (#162) 2025-04-14 01:32:51 +02:00
Petr Mironychev
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
Petr Mironychev
418578743a feat: Prepare widget for chat 2025-04-09 18:27:25 +02:00
Petr Mironychev
56e5ef22f1 doc: Remove commercial support from README.md 2025-04-08 08:14:57 +02:00
Petr Mironychev
e90933d713 fix: Add hack for codellama fim models 2025-04-07 18:55:08 +02:00
Petr Mironychev
5b9c67c2d8 doc: Add sharing opened files feature description to README.md 2025-04-04 22:12:36 +02:00
Petr Mironychev
fe84a2a303 chore: Upgrade plugin version to 0.5.6 2025-04-04 18:39:18 +02:00
Petr Mironychev
62de53c306 chore: Update copyrights 2025-04-04 18:01:02 +02:00
Petr Mironychev
7c6a10936c refactor: Simplify update mechanism (#159) 2025-04-04 17:55:36 +02:00
Petr Mironychev
032c9bbbf3 fix: Add input_extra to llama.cpp validator 2025-04-04 17:16:05 +02:00
Petr Mironychev
8906f98038 fix: Rework copyright searching (#158) 2025-04-04 15:29:36 +02:00
Agron
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
Petr Mironychev
9d2d70fc63 feat: Add sharing opened files with code completion requests (#156) 2025-04-04 10:38:06 +02:00
Petr Mironychev
ffaf6bd61b feat: Add code completion request progress animation (#153) 2025-04-02 21:00:45 +02:00
Petr Mironychev
79218d8412 refactor: Replace singletone for context manager (#151) 2025-04-01 22:29:45 +02:00
Petr Mironychev
7e6e526ac8 refactor: Removed deprecated api keys fields 2025-03-27 02:12:07 +01:00
Petr Mironychev
80646e2af0 feat: Add additional language for handling to CodeCompletion settings (#150) 2025-03-27 02:07:09 +01:00
Petr Mironychev
5808a892c1 fix: Change expected name in test 2025-03-27 00:41:28 +01:00
Petr Mironychev
d58ff90458 fix: Fixed typo in the use of the project name 2025-03-27 00:34:10 +01:00
Petr Mironychev
7d06ab04dc feat: Add RunQtCreator target 2025-03-27 00:29:23 +01:00
Petr Mironychev
9d40e8ca25 chore: Upgrade plugin to 0.5.5 version 2025-03-20 19:02:11 +01:00
Mariusz Jaskółka
5b16c5403a fix: Add authorization while getting installed models (#142) (#147) 2025-03-20 11:33:38 +01:00
Mariusz Jaskółka
4ddbe0b8b9 feat: Support Ollama authorization via BaseAuth (#145) (#146) 2025-03-20 11:14:50 +01:00
Petr Mironychev
f41e063c02 chore: Upgrade plugin version to 0.5.4 2025-03-17 02:51:31 +01:00
Petr Mironychev
9d7d084448 fix: Wrong template replace to first template (#143) 2025-03-17 02:48:18 +01:00
Petr Mironychev
1ca1ffc629 fix: Remove reading from replay leading to crash (#142) 2025-03-17 01:22:27 +01:00
Petr Mironychev
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
Petr Mironychev
91a6a88130 doc: Add default path for installed plugin 2025-03-14 11:35:37 +01:00
Petr Mironychev
be38abc505 chore: upgrade plugin to 0.5.3 (#139) 2025-03-14 10:59:23 +01:00
Petr Mironychev
f2e0afb6b8 fix: Add qml in code handler for processing model answers (#138) 2025-03-14 10:47:30 +01:00
Petr Mironychev
3cf07238fd doc: Update QtC version to 16 2025-03-14 09:32:54 +01:00
Petr Mironychev
b98f85a997 chore: Upgrade plugin to QtCreator 16 (#136) 2025-03-13 16:46:32 +01:00
Petr Mironychev
085659483f doc: Add info about linux compatibility to README.md 2025-03-11 08:34:14 +01:00
Povilas Kanapickas
8a1fd5438e chore: Add tests for LLMClientInterface (#131) 2025-03-10 21:54:17 +01:00
Povilas Kanapickas
78f69e82a5 chore: Checkout submodules when building (#133) 2025-03-10 19:17:30 +01:00
Povilas Kanapickas
3d770f91c7 refactor: Reduce dependency on TextDocument in ContextManager (#128) 2025-03-10 18:06:19 +01:00
Povilas Kanapickas
c724bace06 refactor: Move document access out of prepareContext() (#129) 2025-03-10 17:54:03 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
13cd12b00a chore: Add 3rdparty/inja dependency (#126) 2025-03-10 17:28:25 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Petr Mironychev
6b86637dcb doc: Add llama.cpp description to README.md 2025-03-10 12:07:39 +01:00
Povilas Kanapickas
58c3e26e7f refactor: Decouple LLMClientInterface from ProvidersManager (#120)
This will be needed for tests.
2025-03-10 10:40:51 +01:00
Povilas Kanapickas
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
Petr Mironychev
b6f36d61ae fix: Replace ubuntu-latest to ubuntu 22.04 (#119) 2025-03-10 01:33:03 +01:00
Petr Mironychev
f2f453ccc8 chore: Upgrade plugin to 0.5.1 2025-03-10 01:00:33 +01:00
Petr Mironychev
1bcfd749d5 fix: Change OpenAI model parser conditions 2025-03-10 00:47:16 +01:00
Petr Mironychev
e66f467214 feat: Add llama.cpp provider and fim template (#118) 2025-03-09 22:57:33 +01:00
Povilas Kanapickas
c9a3cdaf25 refactor: Reuse extractFilePathFromRequest() more (#117) 2025-03-08 16:18:44 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
9a5047618d chore: Silence Qt warnings during tests (#110) 2025-03-07 01:58:09 +01:00
Povilas Kanapickas
90beebf2ee Revert "refactor: Move all processing logic to CodeHandler::processText()" (#109) 2025-03-07 01:57:13 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
5536de146c chore: Remove dead code in RequestHandler (#106) 2025-03-06 14:19:01 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Petr Mironychev
0d3493e7f6 feat: Change linux build base to ubuntu 22.04 (#99) 2025-03-06 11:55:04 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
5dceb5cd19 fix: Bring back old behavior of readStrings{After,Before}Cursor setting (#97) 2025-03-05 20:27:16 +01:00
Povilas Kanapickas
69a8aa80d9 refactor: Make DocumentContextReader::prepareContext() testable (#96) 2025-03-05 20:18:59 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
29a3939c64 chore: Add Github Actions job for checking formatting (#92) 2025-03-05 01:46:17 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
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
Povilas Kanapickas
0d22e1866e fix: Add tooltip about where log messages can be seen (#86) 2025-03-03 19:00:25 +01:00
Povilas Kanapickas
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
Povilas Kanapickas
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
Petr Mironychev
e507d7ee17 doc: Add discord link with description 2025-02-27 11:39:57 +01:00
Petr Mironychev
ed55c829af doc: Add Mistral AI and Google AI 2025-02-27 00:20:32 +01:00
Petr Mironychev
d651a246de chore: Upgrade plugin to version 0.5.0 2025-02-26 23:25:19 +01:00
Petr Mironychev
c8e0f3268e fix: End completion position in lsp answer 2025-02-26 23:12:26 +01:00
Petr Mironychev
84025ec843 feat: Separate system prompt for fin and non-fim models 2025-02-26 22:51:38 +01:00
Petr Mironychev
f6fd411b2d feat: Add moving to api key settings page if needed 2025-02-26 21:51:11 +01:00
Petr Mironychev
903eb50e7a fix: Current template description in General Settings 2025-02-26 13:39:49 +01:00
Petr Mironychev
e8f7f031b6 feat: Enabling Position Independent Code 2025-02-26 13:25:29 +01:00
Petr Mironychev
90ae6cd1c0 feat: Add checkbox for disabling chat 2025-02-26 13:18:30 +01:00
Petr Mironychev
2ad0117498 fix: Crash in open General Settings after another settings tab 2025-02-26 11:15:36 +01:00
Petr Mironychev
9d58565de3 feat: Add template description in general settings 2025-02-25 21:58:39 +01:00
Petr Mironychev
8dba9b4baa fix: Saving mistral and google keys to settings 2025-02-25 19:21:44 +01:00
Petr Mironychev
7ba615a72d feat: Add Google AI provider and template 2025-02-25 14:26:56 +01:00
Petr Mironychev
1b06a651f0 fix: Chat item code style 2025-02-24 19:37:29 +01:00
Petr Mironychev
912c3d8c04 feat: Additional marker for user message in chat 2025-02-24 16:54:45 +01:00
Petr Mironychev
e924029ec2 feat: Add filter templates for each provider 2025-02-23 01:41:47 +01:00
Petr Mironychev
d96f44d42c refactor: Rework providers and templates logic 2025-02-22 19:39:28 +01:00
Petr Mironychev
bd25736a55 refactor: Optimize SystemPrompt for code completion 2025-02-16 16:53:03 +01:00
Petr Mironychev
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
Petr Mironychev
7d23d0323f refactor: Improve system prompt and message 2025-02-12 01:47:52 +01:00
Petr Mironychev
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
Petr Mironychev
31133e3378 chore: Update plugin to 0.4.13
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-02 23:28:31 +01:00
Petr Mironychev
a2f15fc843 refactor: Remove experimental Ubuntu 22.04 build from releases 2025-02-02 23:03:51 +01:00
Petr Mironychev
2a0beb6c4c feat: Add language-specific LLM preset configuration
- Add ability to configure separate provider/model/template for specific programming language
- Add UI controls for language preset configuration
- Support custom provider selection per language
- Support custom model selection per language
- Support custom template selection per language
2025-02-02 22:57:18 +01:00
Petr Mironychev
e836b86569 chore: Upgrade plugin to 0.4.12
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:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 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-01 09:37:21 +01:00
Petr Mironychev
288fefebe5 feat: Add CodeLlama QML FIM 2025-02-01 09:36:14 +01:00
Petr Mironychev
528badbf1e Add commercial support to README.md
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:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 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-01-29 15:00:16 +01:00
Petr Mironychev
b789e42602 chore: Upgrade plugin to 0.4.11
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:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 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-01-27 00:55:39 +01:00
Petr Mironychev
4bf955462f feat: Update dialog offer open plugin folder 2025-01-27 00:54:49 +01:00
Petr Mironychev
5b99e68e53 doc: Added file context feature description 2025-01-27 00:14:38 +01:00
Petr Mironychev
0f1b277ef7 chore: Upgrade plugin to 0.4.10 2025-01-26 23:11:58 +01:00
Petr Mironychev
56995c9edf fix: Saving name of chat in native language
- Add button to show chat history folder in system viewer
2025-01-26 23:06:51 +01:00
Petr Mironychev
45aba6b6be fix: Sync editors and chat when sync enable 2025-01-26 22:35:43 +01:00
Petr Mironychev
1dfb3feb96 doc: Update info for QtC 15.0.1 changes
Some checks are pending
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64 cc:gcc cxx:g++ name:Ubuntu Latest GCC os:ubuntu-latest platform:linux_x64]) (push) Waiting to run
Build plugin / ${{ matrix.config.name }} (map[artifact:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 platform:linux_x64]) (push) Waiting to run
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) Waiting to run
Build plugin / ${{ matrix.config.name }} (map[artifact:macOS-universal cc:clang cxx:clang++ name:macOS Latest Clang os:macos-latest platform:mac_x64]) (push) Waiting to run
Build plugin / update_json (push) Blocked by required conditions
Build plugin / release (push) Blocked by required conditions
2025-01-26 10:08:03 +01:00
Petr Mironychev
2c49d45297 fix: Keep name of chat after saving 2025-01-26 10:03:34 +01:00
Petr Mironychev
31145f191b chore: Upgrade plugin to 0.4.9
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:Linux-x64(Ubuntu-22.04-experimental) cc:gcc cxx:g++ name:Ubuntu 22.04 GCC os:ubuntu-22.04 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-01-24 18:27:07 +01:00
Petr Mironychev
9096adde6f fix: Remove installing plugin from update dialog 2025-01-24 18:23:11 +01:00
Petr Mironychev
b8e578d2d7 chore: Update plugin to QtCreator 15.0.1
* fix: Additional check qtc version
* build: Upgrade plugin to QtC 15.0.1
* chore: Upgrade plugin version to 0.4.8
2025-01-24 13:29:44 +01:00
Petr Mironychev
4e45774bce chore: Upgrade version to 0.4.7 2025-01-24 01:52:09 +01:00
Petr Mironychev
928490d31f fix: small style changes 2025-01-24 01:50:56 +01:00
Petr Mironychev
97163cf6c9 fix: Add calculate tokens after clean chat 2025-01-24 01:08:30 +01:00
Petr Mironychev
f85c162692 fix: Improve scroll bar style 2025-01-24 00:59:26 +01:00
Petr Mironychev
258053d826 feat: Add chat file name in top bar 2025-01-24 00:52:10 +01:00
Petr Mironychev
bf63ae5714 refactor: Improve systemPrompt for code completion 2025-01-24 00:37:52 +01:00
Petr Mironychev
ae76850e78 fix: Buttons order in urls dialog on general page 2025-01-24 00:29:15 +01:00
Petr Mironychev
bf3c0b3aa0 feat: Add auto sync open files with model context 2025-01-24 00:22:44 +01:00
Petr Mironychev
9add61c805 feat: Add possibility to link files to the current system prompt
- Add linking files to chat
- Rework tokens counting
2025-01-23 10:17:38 +01:00
Petr Mironychev
add86d2e67 chore: Upgrade version to 0.4.6 2025-01-21 15:05:48 +01:00
Petr Mironychev
a6c909d34d exp: Add ubuntu 22.04 experimental builds 2025-01-21 14:58:44 +01:00
Petr Mironychev
2814dec3e5 fix: Improve file attachment handling
- Add files to existing list instead of replacing when using attach dialog
- Prevent duplicate files from being added to attachment list
2025-01-21 11:33:13 +01:00
Petr Mironychev
1b86b60de8 Add system prompt configuration to readme 2025-01-20 10:00:36 +01:00
Petr Mironychev
4b7f638731 Add setup OpenAI provider 2025-01-19 20:28:06 +01:00
Petr Mironychev
de046f0529 chore: Bump version to 0.4.5 2025-01-19 17:44:11 +01:00
Petr Mironychev
e975e143b1 fix: Handling Ollama messages 2025-01-19 17:32:12 +01:00
Petr Mironychev
c97c0f62e8 fix: Handling full input message from OpenAI compatible providers 2025-01-19 01:16:33 +01:00
Petr Mironychev
61fded34ea feat: add OpenAI provider settings 2025-01-19 00:50:23 +01:00
Petr Mironychev
289a19ac1a feat: Add OpenAI provider and template 2025-01-17 01:22:12 +01:00
Petr Mironychev
43ac662671 fix: Text width in chat item 2025-01-17 00:46:11 +01:00
Petr Mironychev
1d64d2afc9 refactor: Move to using colors from QtC theme palette 2025-01-15 00:05:12 +01:00
Petr Mironychev
9db61119aa feat: Add check plugin update and dialog for update 2025-01-13 20:11:27 +01:00
Petr Mironychev
70481b3116 Remove discord link from README.md 2025-01-08 16:03:00 +01:00
Petr Mironychev
511f5b36eb Upgrade to version 0.4.4
* feat: Add attachments for message
* feat: Support QtC color palette for chat view
* feat: Improve code completion from non-FIM models
* refactor: Removed trimming messages
* chore: Bump version to 0.4.4
2025-01-08 02:05:25 +01:00
Petr Mironychev
35012865c7 refactor: Claude user message and code completion system prompt 2024-12-25 17:50:47 +01:00
Petr Mironychev
f27429aa66 refactor: Move context to separate lib 2024-12-24 22:45:20 +01:00
Petr Mironychev
113d5adcf4 fix: Add description for deprecated settings 2024-12-24 22:00:53 +01:00
Petr Mironychev
30ea89cdc2 chore: Bump version to 0.4.3 2024-12-23 23:36:47 +01:00
Petr Mironychev
13469edce6 doc: Add Claude to README 2024-12-23 23:34:28 +01:00
Petr Mironychev
ee2c3950e8 fix: path to chat file without project 2024-12-23 23:17:43 +01:00
Petr Mironychev
d04e5bc967 Add Claude provider and templates for chat and code (#55)
* feat: Add provider settings
* feat: Add Claude provider
* feat: Add Claude templates
* refactor: Setting input sensitivity
* fix: Back text after read code block
* fix: Add missing system message for ollama fim
2024-12-23 22:22:01 +01:00
Petr Mironychev
d8ef9d0120 refactor: Update issue templates 2024-12-23 18:36:27 +01:00
Petr Mironychev
e544e46d76 feat: Add saving and loading chat history
* feat: Add chat history path
* feat: Add save and load chat
* fix: Change badge width calculation
* refactor: Move chat action to top
* feat: Add autosave of messageReceived
* feat: Add settings for autosave
2024-12-23 18:34:01 +01:00
Petr Mironychev
63f0900511 fix: remove additional message in Ollama Auto Chat template 2024-12-23 16:43:37 +01:00
Petr Mironychev
7dee6f62c0 feat: Add project settings panel 2024-12-21 14:11:45 +01:00
Petr Mironychev
dc06ea2ed5 🔖 chore: Bump version to 0.4.2 2024-12-17 10:33:29 +01:00
Petr Mironychev
fc5e1adc0d 🐛 fix: Fix context for MessageBuilder 2024-12-17 10:32:32 +01:00
Petr Mironychev
93e59fb2dc Add multiline code completion description to README.md 2024-12-17 01:36:28 +01:00
Petr Mironychev
cd2a56cde0 🔖 chore: Bump version to 0.4.1 2024-12-17 00:55:53 +01:00
Petr Mironychev
09cde8fd3d ♻️ refactor: Multiline text suggestion 2024-12-17 00:47:15 +01:00
Petr Mironychev
ac8080542d feat: Add using instruct model in code completion
*  feat: Add MessageBuilder for code completion
*  feat: Add move text from request to comments
*  feat: Add settings for process text of instruct model
* 🐛 fix: Add stop to ollama request validator
* 🐛 fix: Template double delete
2024-12-17 00:35:17 +01:00
Petr Mironychev
7376a11a05 feat: Add request validator 2024-12-15 02:08:35 +01:00
Petr Mironychev
10e8b16caf 🐛 fix: Content description in network request 2024-12-12 21:02:23 +01:00
Petr Mironychev
a38debb140 🐛 fix: Remove test buttons 2024-12-12 15:25:31 +01:00
Petr Mironychev
844ac35a59 🐛 fix: Change name for OpenRouter provider 2024-12-12 15:09:21 +01:00
Petr Mironychev
16b77a5722 feat: Add stream option to settings 2024-12-10 21:46:39 +01:00
Petr Mironychev
c070fd5cfd feat: Add OpenRouter provider 2024-12-10 21:28:15 +01:00
Petr Mironychev
882047d7b2 ♻️ refactor: Improve response handler for LMStudio 2024-12-10 17:13:56 +01:00
Petr Mironychev
b692402897 ♻️ refactor: Improve Ollama response handler 2024-12-10 08:25:30 +01:00
Petr Mironychev
8102ba95f9 Update README.md 2024-12-03 22:33:43 +01:00
Petr Mironychev
f8bb9998ab 🐛 fix: Fix tags 2024-12-03 21:52:41 +01:00
Petr Mironychev
6dab055ca2 🐛 fix: FIx json error formating 2024-12-03 21:46:24 +01:00
Petr Mironychev
7b31fff9f2 🐛 fix: Update qodeassist json 2024-12-03 21:32:18 +01:00
Petr Mironychev
be9156fd0e 🐛 fix: Add plugins data 2024-12-03 21:16:39 +01:00
Petr Mironychev
657413344d Add icons to README 2024-12-03 21:11:31 +01:00
Petr Mironychev
5f3deb44b9 doc: temporary fix for qtc version 2024-12-03 12:00:03 +01:00
Petr Mironychev
55e2b24b8d Upgrade plugin to Qt Creator 15
* 🐛 fix: Change plugin configs
* 🐛 fix: Update Button aspect api
* 🐛 fix: Temproary fix for LLMSuggestions
* 🐛 fix: Update github actions
* 🔖 chore: Upgrade version in README
2024-12-03 11:15:35 +01:00
Petr Mironychev
76c17f03dd feat: Add model-template compatibility table 2024-11-26 14:03:28 +01:00
Petr Mironychev
19c25043fb 🔖 chore: Bump version to 0.3.10 2024-11-26 11:48:10 +01:00
Petr Mironychev
56b5ea8e68 feat: Improve OpenAI message handling 2024-11-26 11:43:51 +01:00
Petr Mironychev
b475f15e3d feat: Improve system prompt for code completion 2024-11-26 11:29:20 +01:00
Petr Mironychev
31f4516e7b feat: Add removing codeblock wrappers from code completion 2024-11-26 11:26:50 +01:00
Petr Mironychev
bfdbc755e3 🐛 fix: Move api key from request json to config 2024-11-26 10:52:47 +01:00
Petr Mironychev
30964d90d5 🐛 fix: Change format for context in system prompt 2024-11-26 10:15:20 +01:00
Petr Mironychev
1261f913bb ♻️ refactor: Rework currents and add new templates
Add Alpaca, Llama3, LLama2, ChatML templates
2024-11-26 00:28:27 +01:00
Petr Mironychev
36d5242a1f 🐛 fix: Removing message from chat after complete receiving 2024-11-25 23:00:53 +01:00
Petr Mironychev
6503887091 Upgrade to version 0.3.9 2024-11-23 21:59:35 +01:00
Petr Mironychev
50087aa744 Change configure part in description
- Replace codellama to qwen models
- Add prefer auto template for ollama provider
2024-11-23 21:55:57 +01:00
Petr Mironychev
4f2dc0c450 feat: Add Ollama auto template for chat 2024-11-23 21:15:34 +01:00
Petr Mironychev
80fe388bdd feat: Add automatic template handling for Ollama models (#43)
* feat: Add automatic template handling for Ollama models

- Add OllamaAutoFim
- Use native Ollama API format when possible
- Remove need for manual template selection for most Ollama models
- Default to model-specific format from Ollama modelfile
- Fallback to manual template selection if needed

This change simplifies configuration by automatically using
the correct template format for each Ollama model.
2024-11-23 19:37:55 +01:00
Petr Mironychev
8375d85f7d Upgrade to 0.3.8 2024-11-16 15:42:10 +01:00
Petr Mironychev
54b2cc7011 Update FUNDING.yml 2024-11-16 15:34:22 +01:00
Petr Mironychev
f209cb75a2 Impove UX general setting by added helpers dialogs for user (#42)
- Added dialogs for selecting url, model and custom model when provider doesn't provide model list or setup of qode assist is not finishing
2024-11-16 15:25:28 +01:00
Petr Mironychev
5e813ba402 Fix systemPrompt and context working 2024-11-16 10:20:57 +01:00
Petr Mironychev
7af8fc2ddc Add projects code style improvements
* Add qmlls to gitignore
* Fix qmlformat file
* Add project code style description to README.md
2024-11-16 00:47:57 +01:00
Petr Mironychev
46300f7635 Update issue templates 2024-11-16 00:35:24 +01:00
Petr Mironychev
0a1c941d8b Fix selecting chat models
Old code linked to fim provider, changed to chat provider now
2024-11-12 08:15:06 +01:00
Petr Mironychev
f86182408d Add clang-format and qmlformat files 2024-11-12 00:12:18 +01:00
Petr Mironychev
252db4c5f7 Add clang-format and qmlformat files 2024-11-12 00:07:12 +01:00
Petr Mironychev
e5af3a2884 Merge pull request #36 from Palm1r/fix-pr30-building
Fix pr30 build
2024-11-11 21:51:08 +01:00
Petr Mironychev
bb543d1f40 Fix pr30 build 2024-11-11 21:45:38 +01:00
Petr Mironychev
a184916d7b Fix double call building in build_cmake.yml 2024-11-11 21:43:46 +01:00
Petr Mironychev
7ad8ddfee4 Merge pull request #30 from SidneyCogdill/patch/fix-qml-warnings
Fix all warnings in QML
2024-11-11 21:30:26 +01:00
Petr Mironychev
0ed6fb4a6b Add pull_request to build_cmake.yml 2024-11-11 21:27:31 +01:00
Petr Mironychev
00fce5db99 Version 0.3.7
Fix settings category icon and name
2024-11-11 11:30:37 +01:00
Petr Mironychev
251a9bae03 Fix settings category icon and name 2024-11-11 11:26:11 +01:00
Petr Mironychev
3dba9d7abe Fix link to discord README.md 2024-11-11 11:03:48 +01:00
Petr Mironychev
45b0f3f18e Add discord link to README.md 2024-11-11 10:50:53 +01:00
Petr Mironychev
4432d4019d Version 0.3.6
Upgrade plugin version to 0.3.6
2024-11-11 08:29:12 +01:00
Petr Mironychev
f679d76d43 Upgrade version to 0.3.6 2024-11-11 08:25:05 +01:00
Petr Mironychev
29f94561ef Fix openai api key
Restore the ability to use API key
2024-11-11 07:21:18 +01:00
SidneyCogdill
cd6c766ed2 Restore the ability to use API key 2024-11-11 14:09:32 +08:00
SidneyCogdill
6d3bc362b3 Fix all warnings in QML 2024-11-11 11:18:37 +08:00
Petr Mironychev
87393b681f Version 0.3.5
- Rework General, Code Completion and Chat Assist pages in settings 
- Add Sharing current open file with model and settings for default behavior for this
- Fixed a bug on WindowsOS where settings were set incorrectly
2024-11-11 01:08:18 +01:00
Petr Mironychev
5d496fee58 Upgrade version 2024-11-11 01:01:26 +01:00
Petr Mironychev
9902623ba0 Fix problem with wrong reading settings 2024-11-11 01:00:54 +01:00
Petr Mironychev
61f1f0ae4f Add sharing current file with model 2024-11-11 00:52:49 +01:00
Petr Mironychev
bc93bce03b Adapt new settings 2024-11-11 00:03:38 +01:00
Petr Mironychev
85d039cbd5 Rework Chat Assistant Settings 2024-11-10 22:50:47 +01:00
Petr Mironychev
2acaef553d Rework Code Completion Settings 2024-11-10 21:21:37 +01:00
Petr Mironychev
b141e54e3e Rework General Settings 2024-11-10 20:42:10 +01:00
Petr Mironychev
1ec6098210 Move ButtonAspect 2024-11-10 18:56:57 +01:00
Petr Mironychev
9c945f066b Rework README.md
- Add badges
- Add table of contents
2024-10-21 08:28:20 +02:00
Petr Mironychev
4a82e9c046 Version 0.3.4
Add support Qwen model for autocompete(FIM)
based on qwen2.5-coder:7b-base
2024-10-21 01:58:55 +02:00
Petr Mironychev
838d69623c Upgrade to 0.3.4 2024-10-21 01:54:27 +02:00
Petr Mironychev
693e429bdd Fix name in bar badge 2024-10-19 20:11:09 +02:00
Petr Mironychev
496d8feb66 Add Qwen fim template 2024-10-19 19:13:42 +02:00
Petr Mironychev
40a568ebd9 Version 0.3.3
- Add streaming response to chat
- Add stopping chat request and button
2024-10-17 00:10:47 +02:00
Petr Mironychev
5b43eb4fd2 Increase default max tokens for chat before deleting first messages 2024-10-17 00:05:31 +02:00
Petr Mironychev
9c2516cd4c Add stopping chat requests and button 2024-10-17 00:03:12 +02:00
Petr Mironychev
2257e6e45f Fix unbehavior settings of provider and template 2024-10-17 00:02:14 +02:00
Petr Mironychev
80eda8c167 Add stream text to chat 2024-10-16 22:51:34 +02:00
Petr Mironychev
3db2691114 Upgrade version 2024-10-16 22:44:51 +02:00
Petr Mironychev
bf518b4a01 Version 0.3.2
Add StarCoder2 instruct support
2024-10-16 10:50:19 +02:00
Petr Mironychev
46829720d8 Add StarCoder2 instruct support 2024-10-16 10:45:48 +02:00
Petr Mironychev
9158a3ac0d Add Llama support to README.md 2024-10-14 21:52:36 +02:00
Petr Mironychev
d6e02d9d2a Version 0.3.1
Improve chat text input
Add Llama chat support
Fix monospace font
2024-10-14 21:41:04 +02:00
Petr Mironychev
9c8cac4e3a Upgrade to version 0.3.1 2024-10-14 21:35:49 +02:00
Petr Mironychev
965af4a945 Add Llama chat support 2024-10-14 21:35:17 +02:00
Petr Mironychev
95f29fefc7 Fix monospace font 2024-10-14 21:25:18 +02:00
Petr Mironychev
1dd50b6c83 Replace textinput to textfield 2024-10-14 21:18:48 +02:00
Petr Mironychev
146e772514 Update README.md 2024-10-14 01:38:50 +02:00
Petr Mironychev
4b851f1662 Update README.md for 0.3.0 2024-10-14 01:36:48 +02:00
Petr Mironychev
6fea300825 Version 0.3.0
- new QML chat in outpane and in side panels
- qwen chat model template
- long long list of fixes
2024-10-14 00:27:43 +02:00
Petr Mironychev
14bf0e6c94 Upgrade plugin version to 0.3.0 2024-10-14 00:22:24 +02:00
Petr Mironychev
0c045e65df Upgrade to version 0.3.0
new QML Chat
Qwen chat model
2024-10-14 00:10:24 +02:00
Petr Mironychev
15138b4644 Update FUNDING.yml 2024-10-13 14:00:18 +02:00
Petr Mironychev
5c98de7440 Add components for chat 2024-10-07 00:53:35 +02:00
Petr Mironychev
b808d0ec10 Remove temporary code 2024-10-07 00:49:49 +02:00
Petr Mironychev
30fcd7e019 Add ChatRootView QML item 2024-10-06 18:47:10 +02:00
Petr Mironychev
7442256bab Add ChatModel 2024-10-06 18:19:12 +02:00
Petr Mironychev
8be279a5fd Add base qml chat view to navigation panel 2024-10-06 17:54:00 +02:00
Petr Mironychev
d77e13cddb Change to scope pointer 2024-10-06 16:03:50 +02:00
Petr Mironychev
162c068431 Upgrade version to 0.2.4 2024-10-05 20:50:15 +02:00
Petr Mironychev
4e8ff55355 Add QtCreator Version Compatibility to README 2024-10-03 17:36:45 +02:00
Petr Mironychev
8df21e96bd Add QtCreator Version Compatibility to README 2024-10-03 17:28:58 +02:00
Petr Mironychev
57bec94ee4 Version 0.2.3, Update to QtCreator 14.0.2 2024-10-03 16:44:22 +02:00
Petr Mironychev
1760a2d5ff Upgrade QtCreator to 14.0.2
Add no print support

Test disabling qt6 print component

Clean unnecessary code

Add library
2024-10-03 15:30:16 +02:00
Petr Mironychev
1649a246e1 Version 0.2.2
- Improve chat history
- Fix system prompt
- Fix ollama model livetime
- Fix providers and prompt default value
2024-10-02 22:32:59 +02:00
Petr Mironychev
0ab4b51520 Upgrade plugin version 2024-10-02 22:28:52 +02:00
Petr Mironychev
d235d0fcdf Fix providers and prompt default value 2024-10-02 22:25:53 +02:00
Petr Mironychev
1cbde3d55b Fix ollama model livetime 2024-10-02 21:44:32 +02:00
Petr Mironychev
9903ac8f7b Fix system prompt for FIM 2024-10-02 21:44:15 +02:00
Petr Mironychev
fbe363689f Change message history 2024-10-01 00:26:28 +02:00
Petr Mironychev
d7f0cc92e6 Version 0.2.1
- Fix getting models list from providers
- Fix clearing message history in chat
- Remove unnecessary logs
2024-09-24 09:31:40 +02:00
Petr Mironychev
8311db5b08 Removed unessary logs 2024-09-24 09:28:53 +02:00
Petr Mironychev
cd6dd94cd2 Upgrade version to 0.2.1 2024-09-24 09:20:26 +02:00
Petr Mironychev
b559a17407 Fix getting models list 2024-09-24 08:59:26 +02:00
Petr Mironychev
9745997952 Fix clear message history 2024-09-23 23:54:36 +02:00
Petr Mironychev
aac58d6933 Version 0.2.0
- Separate handling messages from LSP and LLM providers
- Chat basic components and settings
- Templates for chat
2024-09-23 01:56:27 +02:00
Petr Mironychev
93c69df1b9 Fix typo 2024-09-23 00:19:38 +02:00
Petr Mironychev
4b0b589d0e Upgrade version 2024-09-23 00:12:25 +02:00
Petr Mironychev
04c44f5916 Add basic chat widgets and functionality 2024-09-23 00:10:23 +02:00
Petr Mironychev
6e56646b4c Version 0.1.3
Remove unnecessary log
Improve settings
2024-09-15 01:33:48 +02:00
Petr Mironychev
c89fe1451b Improve settings accessibility 2024-09-15 01:20:26 +02:00
Petr Mironychev
938636ab48 Remove unnecessary log 2024-09-15 01:15:53 +02:00
410 changed files with 41570 additions and 3453 deletions

108
.clang-format Normal file
View File

@@ -0,0 +1,108 @@
# .clang-format from Qt Creator
# https://github.com/qt-creator/qt-creator/blob/master/.clang-format
#
# yaml-language-server: $schema=https://json.schemastore.org/clang-format.json
#
---
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: AlwaysBreak
AlignConsecutiveAssignments: None
AlignConsecutiveDeclarations: None
AlignEscapedNewlines: DontAlign
AlignOperands: Align
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: Never
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: Inline
AllowShortIfStatementsOnASingleLine: Never
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: false
AlwaysBreakTemplateDeclarations: Yes
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterClass: true
AfterControlStatement: Never
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterObjCDeclaration: false
AfterStruct: true
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: false
SplitEmptyNamespace: false
BreakBeforeBinaryOperators: All
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeComma
BreakAfterJavaFieldAnnotations: false
BreakStringLiterals: true
ColumnLimit: 100
CommentPragmas: '^ IWYU pragma:'
CompactNamespaces: false
ConstructorInitializerAllOnOneLineOrOnePerLine: false
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 4
Cpp11BracedListStyle: true
DerivePointerAlignment: false
DisableFormat: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: true
ForEachMacros:
- forever # avoids { wrapped to next line
- foreach
- Q_FOREACH
- BOOST_FOREACH
IncludeCategories:
- Regex: '^<Q.*'
Priority: 200
IncludeIsMainRegex: '(Test)?$'
IndentCaseLabels: false
IndentWidth: 4
IndentWrappedFunctionNames: false
InsertBraces: false
JavaScriptQuotes: Leave
JavaScriptWrapImports: true
KeepEmptyLinesAtTheStartOfBlocks: false
# Do not add QT_BEGIN_NAMESPACE/QT_END_NAMESPACE as this will indent lines in between.
MacroBlockBegin: ""
MacroBlockEnd: ""
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
ObjCBlockIndentWidth: 4
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PenaltyBreakAssignment: 88
PenaltyBreakBeforeFirstCallParameter: 300
PenaltyBreakComment: 500
PenaltyBreakFirstLessLess: 400
PenaltyBreakString: 600
PenaltyExcessCharacter: 50
PenaltyReturnTypeOnItsOwnLine: 300
PointerAlignment: Right
ReflowComments: false
SortIncludes: CaseSensitive
SortUsingDeclarations: true
SpaceAfterCStyleCast: true
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: Never
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: c++17
TabWidth: 4
UseTab: Never

4
.github/FUNDING.yml vendored
View File

@@ -3,7 +3,7 @@
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: petrmdev
ko_fi: qodeassist
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
@@ -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']

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Log**
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

109
.github/scripts/plugin.json vendored Normal file
View File

@@ -0,0 +1,109 @@
{
"name": "QodeAssist",
"vendor": "Petr Mironychev",
"tags": [
"code assistant",
"llm",
"ai"
],
"compatibility": "Qt 6.8.3",
"platforms": [
"Windows",
"macOS",
"Linux"
],
"license": "GPLv3",
"version": "0.5.11",
"status": "draft",
"is_pack": false,
"released_at": null,
"version_history": [
{
"version": "0.4.0",
"is_latest": false,
"released_at": "2024-01-24T15:00:00Z"
},
{
"version": "0.5.2",
"is_latest": false,
"released_at": "2025-03-13T17:00:00Z"
},
{
"version": "0.5.3",
"is_latest": false,
"released_at": "2025-03-14T11:00:00Z"
},
{
"version": "0.5.4",
"is_latest": false,
"released_at": "2025-03-17T03:00:00Z"
},
{
"version": "0.5.5",
"is_latest": false,
"released_at": "2025-03-20T19:00:00Z"
},
{
"version": "0.5.6",
"is_latest": false,
"released_at": "2025-04-04T19:00:00Z"
},
{
"version": "0.5.7",
"is_latest": false,
"released_at": "2025-04-14T01:00:00Z"
},
{
"version": "0.5.8",
"is_latest": false,
"released_at": "2025-04-17T10:00:00Z"
},
{
"version": "0.5.9",
"is_latest": false,
"released_at": "2025-04-21T10:00:00Z"
},
{
"version": "0.5.10",
"is_latest": false,
"released_at": "2025-04-24T10:00:00Z"
},
{
"version": "0.5.11",
"is_latest": false,
"released_at": "2025-04-24T21:00:00Z"
},
{
"version": "0.5.12",
"is_latest": true,
"released_at": "2025-05-01T17:00:00Z"
}
],
"icon": "https://github.com/user-attachments/assets/dc336712-83cb-440d-8761-8d0a31de898d",
"small_icon": "https://github.com/user-attachments/assets/8ec241bf-3186-452e-b8db-8d70543c2f41",
"description_paragraphs": [
{
"header": "Description",
"text": [
"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."
]
}
],
"description_links": [
{
"url": "https://github.com/Palm1r/QodeAssist",
"link_text": "Site"
}
],
"description_images": [
{
"url": "https://github.com/user-attachments/assets/255a52f1-5cc0-4ca3-b05c-c4cf9cdbe25a",
"image_label": "Code Completion"
}
],
"copyright": "(C) Petr Mironychev",
"download_history": {
"download_count": 0
},
"plugin_sets": []
}

147
.github/scripts/registerPlugin.js vendored Normal file
View File

@@ -0,0 +1,147 @@
const fs = require('fs');
const path = require('path');
const updatePluginData = (plugin, env, pluginQtcData) => {
const dictionary_platform = {
'Windows': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Windows-x64.7z`,
'Linux': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-Linux-x64.7z`,
'macOS': `${env.PLUGIN_DOWNLOAD_URL}/${env.PLUGIN_NAME}-${env.QT_CREATOR_VERSION}-macOS-universal.7z`
};
plugin.core_compat_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.core_version = env.QT_CREATOR_VERSION_INTERNAL;
plugin.status = "draft";
plugin.plugins.forEach(pluginsEntry => {
pluginsEntry.url = dictionary_platform[plugin.host_os];
pluginsEntry.meta_data = pluginQtcData;
});
return plugin;
};
const createNewPluginData = (env, platform, pluginQtcData) => {
const pluginJson = {
"status": "draft",
"core_compat_version": "<placeholder>",
"core_version": "<placeholder>",
"host_os": platform,
"host_os_version": "0", // TODO: pass the real data
"host_os_architecture": "x86_64", // TODO: pass the real data
"plugins": [
{
"url": "",
"size": 5000, // TODO: check if it is needed, pass the real data
"meta_data": {},
"dependencies": []
}
]
};
updatePluginData(pluginJson, env, pluginQtcData);
return pluginJson;
}
const updateServerPluginJson = (endJsonData, pluginQtcData, env) => {
// Update the global data in mainData
endJsonData.name = pluginQtcData.Name;
endJsonData.vendor = pluginQtcData.Vendor;
endJsonData.version = pluginQtcData.Version;
endJsonData.copyright = pluginQtcData.Copyright;
endJsonData.status = "draft";
endJsonData.version_history[0].version = pluginQtcData.Version;
endJsonData.description_paragraphs = [
{
header: "Description",
text: [
pluginQtcData.Description
]
}
];
let found = false;
// Update or Add the plugin data for the current Qt Creator version
for (const plugin of endJsonData.plugin_sets) {
if (plugin.core_compat_version === env.QT_CREATOR_VERSION_INTERNAL) {
updatePluginData(plugin, env, pluginQtcData);
found = true;
}
}
if (!found) {
for (const platform of ['Windows', 'Linux', 'macOS']) {
endJsonData.plugin_sets.push(createNewPluginData(env, platform, pluginQtcData));
}
}
// Save the updated JSON file
const serverPluginJsonPath = path.join(__dirname, `${env.PLUGIN_NAME}.json`);
fs.writeFileSync(serverPluginJsonPath, JSON.stringify(endJsonData, null, 2), 'utf8');
};
const request = async (type, url, token, data) => {
const response = await fetch(url, {
method: type,
headers: {
'Authorization': `Bearer ${token}`,
'accept': 'application/json',
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
});
if (!response.ok) {
const errorResponse = await response.json();
console.error(`${type} Request Error Response:`, errorResponse); // Log the error response
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
const put = (url, token, data) => request('PUT', url, token, data)
const post = (url, token, data) => request('POST', url, token, data)
const get = (url, token) => request('GET', url, token)
const purgeCache = async (env) => {
try {
await post(`${env.API_URL}api/v1/cache/purgeall`, env.TOKEN, {});
console.log('Cache purged successfully');
} catch (error) {
console.error('Error:', error);
}
};
async function main() {
const env = {
PLUGIN_DOWNLOAD_URL: process.env.PLUGIN_DOWNLOAD_URL || process.argv[2],
PLUGIN_NAME: process.env.PLUGIN_NAME || process.argv[3],
QT_CREATOR_VERSION: process.env.QT_CREATOR_VERSION || process.argv[4],
QT_CREATOR_VERSION_INTERNAL: process.env.QT_CREATOR_VERSION_INTERNAL || process.argv[5],
TOKEN: process.env.TOKEN || process.argv[6],
API_URL: process.env.API_URL || process.argv[7] || ''
};
const pluginQtcData = require(`../../${env.PLUGIN_NAME}-origin/${env.PLUGIN_NAME}.json`);
const templateFileData = require('./plugin.json');
if (env.API_URL === '') {
updateServerPluginJson(templateFileData, pluginQtcData, env);
process.exit(0);
}
const response = await get(`${env.API_URL}api/v1/admin/extensions?search=${env.PLUGIN_NAME}`, env.TOKEN);
if (response.items.length > 0 && response.items[0].extension_id !== '') {
const pluginId = response.items[0].extension_id;
console.log('Plugin found. Updating the plugin');
updateServerPluginJson(response.items[0], pluginQtcData, env);
await put(`${env.API_URL}api/v1/admin/extensions/${pluginId}`, env.TOKEN, response.items[0]);
} else {
console.log('No plugin found. Creating a new plugin');
updateServerPluginJson(templateFileData, pluginQtcData, env);
await post(`${env.API_URL}api/v1/admin/extensions`, env.TOKEN, templateFileData);
}
// await purgeCache(env);
}
main().then(() => console.log('JSON file updated successfully'));

View File

@@ -1,19 +1,24 @@
name: Build plugin
on: [push]
on:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
env:
PLUGIN_NAME: QodeAssist
QT_VERSION: 6.7.2
QT_CREATOR_VERSION: 14.0.0
QT_CREATOR_SNAPSHOT: NO
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 }}
@@ -23,76 +28,59 @@ jobs:
- {
name: "Windows Latest MSVC", artifact: "Windows-x64",
os: windows-latest,
platform: windows_x64,
cc: "cl", cxx: "cl",
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++"
}
- {
name: "macOS Latest Clang", artifact: "macOS-universal",
os: macos-latest,
platform: mac_x64,
cc: "clang", cxx: "clang++"
}
qt_config:
- {
qt_version: "6.10.1",
qt_creator_version: "18.0.2"
}
- {
qt_version: "6.10.3",
qt_creator_version: "19.0.1"
}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955
with:
submodules: recursive
- name: Checkout submodules
id: git
shell: cmake -P {0}
run: |
if (${{github.ref}} MATCHES "tags/v(.*)")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}\n")
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${CMAKE_MATCH_1}")
else()
file(APPEND "$ENV{GITHUB_OUTPUT}" "tag=${{github.run_id}}\n")
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
shell: cmake -P {0}
run: |
set(cmake_version "$ENV{CMAKE_VERSION}")
set(ninja_version "$ENV{NINJA_VERSION}")
uses: lukka/get-cmake@2ecc21724e5215b0e567bc399a2602d2ecb48541
with:
cmakeVersion: ${{ env.CMAKE_VERSION }}
ninjaVersion: ${{ env.NINJA_VERSION }}
if ("${{ runner.os }}" STREQUAL "Windows")
set(ninja_suffix "win.zip")
set(cmake_suffix "windows-x86_64.zip")
set(cmake_dir "cmake-${cmake_version}-windows-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(ninja_suffix "linux.zip")
set(cmake_suffix "linux-x86_64.tar.gz")
set(cmake_dir "cmake-${cmake_version}-linux-x86_64/bin")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(ninja_suffix "mac.zip")
set(cmake_suffix "macos-universal.tar.gz")
set(cmake_dir "cmake-${cmake_version}-macos-universal/CMake.app/Contents/bin")
endif()
set(ninja_url "https://github.com/ninja-build/ninja/releases/download/v${ninja_version}/ninja-${ninja_suffix}")
file(DOWNLOAD "${ninja_url}" ./ninja.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./ninja.zip)
set(cmake_url "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-${cmake_suffix}")
file(DOWNLOAD "${cmake_url}" ./cmake.zip SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ./cmake.zip)
# Add to PATH environment variable
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/${cmake_dir}" cmake_dir)
set(path_separator ":")
if ("${{ runner.os }}" STREQUAL "Windows")
set(path_separator ";")
endif()
file(APPEND "$ENV{GITHUB_PATH}" "$ENV{GITHUB_WORKSPACE}${path_separator}${cmake_dir}")
if (NOT "${{ runner.os }}" STREQUAL "Windows")
execute_process(
COMMAND chmod +x ninja
COMMAND chmod +x ${cmake_dir}/cmake
)
endif()
- name: Install system libs
- name: Install dependencies
shell: cmake -P {0}
run: |
if ("${{ runner.os }}" STREQUAL "Linux")
@@ -100,7 +88,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)
@@ -112,14 +106,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_msvc2019_64")
set(qt_dir_prefix "${qt_version}/msvc2019_64")
set(qt_package_suffix "-Windows-Windows_10_22H2-MSVC2019-Windows-Windows_10_22H2-X86_64")
set(qt_package_arch_suffix "win64_msvc2022_64")
set(qt_dir_prefix "${qt_version}/msvc2022_64")
if (qt_creator_version VERSION_GREATER_EQUAL "18.0.0")
set(qt_package_suffix "-Windows-Windows_11_24H2-MSVC2022-Windows-Windows_11_24H2-X86_64")
else()
set(qt_package_suffix "-Windows-Windows_11_23H2-MSVC2022-Windows-Windows_11_23H2-X86_64")
endif()
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(url_os "linux_x64")
if (qt_version VERSION_LESS "6.7.0")
@@ -128,15 +127,23 @@ 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_8-GCC-Linux-RHEL_8_8-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_13-Clang-MacOS-MacOS_13-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}")
set(qt_base_url "https://download.qt.io/online/qtsdkrepository/${url_os}/desktop/qt6_${qt_version_dotless}/qt6_${qt_version_dotless}")
file(DOWNLOAD "${qt_base_url}/Updates.xml" ./Updates.xml SHOW_PROGRESS)
file(READ ./Updates.xml updates_xml)
@@ -146,7 +153,7 @@ jobs:
file(MAKE_DIRECTORY qt6)
# Save the path for other steps
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6/${qt_dir_prefix}" qt_dir)
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qt6" qt_dir)
file(APPEND "$ENV{GITHUB_OUTPUT}" "qt_dir=${qt_dir}")
message("Downloading Qt to ${qt_dir}")
@@ -156,7 +163,7 @@ jobs:
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../${archive} WORKING_DIRECTORY qt6)
endfunction()
foreach(package qtbase qtdeclarative)
foreach(package qtbase qtdeclarative qttools qtsvg)
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
@@ -165,11 +172,17 @@ jobs:
foreach(package qt5compat qtshadertools)
downloadAndExtract(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
"${qt_base_url}/qt.qt6.${qt_version_dotless}.addons.${package}.${qt_package_arch_suffix}/${qt_package_version}${package}${qt_package_suffix}.7z"
${package}.7z
)
endforeach()
function(downloadAndExtractLibicu url archive)
message("Downloading ${url}")
file(DOWNLOAD "${url}" ./${archive} SHOW_PROGRESS)
execute_process(COMMAND ${CMAKE_COMMAND} -E tar xvf ../../${archive} WORKING_DIRECTORY qt6/lib)
endfunction()
# uic depends on libicu*.so
if ("${{ runner.os }}" STREQUAL "Linux")
if (qt_version VERSION_LESS "6.7.0")
@@ -177,47 +190,26 @@ jobs:
else()
set(uic_suffix "Rhel8.6-x86_64")
endif()
downloadAndExtract(
downloadAndExtractLibicu(
"${qt_base_url}/qt.qt6.${qt_version_dotless}.${qt_package_arch_suffix}/${qt_package_version}icu-linux-${uic_suffix}.7z"
icu.7z
)
endif()
- name: Download Qt Creator
uses: qt-creator/install-dev-package@1460787a21551eb3d867b0de30e8d3f1aadef5ac
with:
version: ${{ matrix.qt_config.qt_creator_version }}
unzip-to: 'qtcreator'
platform: ${{ matrix.config.platform }}
- name: Extract Qt Creator
id: qt_creator
shell: cmake -P {0}
run: |
string(REGEX MATCH "([0-9]+.[0-9]+).[0-9]+" outvar "$ENV{QT_CREATOR_VERSION}")
set(qtc_base_url "https://download.qt.io/official_releases/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source")
set(qtc_snapshot "$ENV{QT_CREATOR_SNAPSHOT}")
if (qtc_snapshot)
set(qtc_base_url "https://download.qt.io/snapshots/qtcreator/${CMAKE_MATCH_1}/$ENV{QT_CREATOR_VERSION}/installer_source/${qtc_snapshot}")
endif()
if ("${{ runner.os }}" STREQUAL "Windows")
set(qtc_platform "windows_x64")
elseif ("${{ runner.os }}" STREQUAL "Linux")
set(qtc_platform "linux_x64")
elseif ("${{ runner.os }}" STREQUAL "macOS")
set(qtc_platform "mac_x64")
endif()
file(TO_CMAKE_PATH "$ENV{GITHUB_WORKSPACE}/qtcreator" qtc_dir)
# Save the path for other steps
file(APPEND "$ENV{GITHUB_OUTPUT}" "qtc_dir=${qtc_dir}")
file(MAKE_DIRECTORY qtcreator)
message("Downloading Qt Creator from ${qtc_base_url}/${qtc_platform}")
foreach(package qtcreator qtcreator_dev)
file(DOWNLOAD
"${qtc_base_url}/${qtc_platform}/${package}.7z" ./${package}.7z SHOW_PROGRESS)
execute_process(COMMAND
${CMAKE_COMMAND} -E tar xvf ../${package}.7z WORKING_DIRECTORY qtcreator)
endforeach()
- name: Build
shell: cmake -P {0}
run: |
@@ -255,7 +247,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 }}"
@@ -271,19 +263,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
- name: Run unit tests
if: startsWith(matrix.config.os, 'ubuntu')
run: |
xvfb-run ./build/build/test/QodeAssistTest
release:
if: contains(github.ref, 'tags/v')
runs-on: ubuntu-latest
needs: build
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
@@ -292,9 +289,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:

9
.gitignore vendored
View File

@@ -34,6 +34,7 @@ Thumbs.db
*.rc
/.qmake.cache
/.qmake.stash
.qmlls.ini
# qtcreator generated files
*.pro.user*
@@ -72,4 +73,10 @@ CMakeLists.txt.user*
*.dll
*.exe
/build
/build
/.qodeassist
/.cursor
/.vscode
.qtc_clangd/compile_commands.json
CLAUDE.md
/.claude

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "sources/external/llmqore"]
path = sources/external/llmqore
url = https://github.com/Palm1r/llmqore.git

3
.qmlformat.ini Normal file
View File

@@ -0,0 +1,3 @@
[General]
IndentWidth=4
NewlineType=native

View File

@@ -8,20 +8,63 @@ 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 Widgets REQUIRED)
find_package(Qt6 COMPONENTS Core Gui Quick Widgets Network Svg 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(sources/external/llmqore)
add_subdirectory(pluginllmcore)
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
QtCreator::Core
QtCreator::LanguageClient
QtCreator::TextEditor
QtCreator::ProjectExplorer
QtCreator::CppEditor
DEPENDS
Qt::Core
Qt::Gui
Qt::Quick
Qt::Widgets
Qt::Network
Qt::Svg
QtCreator::ExtensionSystem
QtCreator::Utils
QtCreator::ProjectExplorer
QtCreator::CPlusPlus
LLMQore
PluginLLMCore
QodeAssistChatViewplugin
SOURCES
.github/workflows/build_cmake.yml
.github/workflows/README.md
@@ -30,29 +73,105 @@ add_qtc_plugin(QodeAssist
QodeAssistConstants.hpp
QodeAssisttr.h
LLMClientInterface.hpp LLMClientInterface.cpp
PromptTemplateManager.hpp PromptTemplateManager.cpp
templates/PromptTemplate.hpp
templates/CodeLLamaTemplate.hpp
templates/StarCoder2Template.hpp
templates/DeepSeekCoderV2.hpp
templates/CustomTemplate.hpp
providers/LLMProvider.hpp
RefactorContextHelper.hpp
templates/Templates.hpp
templates/CodeLlamaFim.hpp
templates/Ollama.hpp
templates/Claude.hpp
templates/OpenAI.hpp
templates/MistralAI.hpp
templates/StarCoder2Fim.hpp
templates/Qwen25CoderFIM.hpp
templates/OpenAICompatible.hpp
templates/Llama3.hpp
templates/ChatML.hpp
templates/Alpaca.hpp
templates/Llama2.hpp
templates/CodeLlamaQMLFim.hpp
templates/GoogleAI.hpp
templates/LlamaCppFim.hpp
templates/Qwen3CoderFIM.hpp
templates/OpenAIResponses.hpp
providers/Providers.hpp
providers/OllamaProvider.hpp providers/OllamaProvider.cpp
providers/OllamaCompatProvider.hpp providers/OllamaCompatProvider.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/LMStudioResponsesProvider.hpp providers/LMStudioResponsesProvider.cpp
providers/OpenAICompatProvider.hpp providers/OpenAICompatProvider.cpp
LLMProvidersManager.hpp LLMProvidersManager.cpp
providers/OpenRouterAIProvider.hpp providers/OpenRouterAIProvider.cpp
providers/GoogleAIProvider.hpp providers/GoogleAIProvider.cpp
providers/LlamaCppProvider.hpp providers/LlamaCppProvider.cpp
providers/CodestralProvider.hpp providers/CodestralProvider.cpp
providers/OpenAIResponsesProvider.hpp providers/OpenAIResponsesProvider.cpp
QodeAssist.qrc
LSPCompletion.hpp
LLMSuggestion.hpp LLMSuggestion.cpp
RefactorSuggestion.hpp RefactorSuggestion.cpp
RefactorSuggestionHoverHandler.hpp RefactorSuggestionHoverHandler.cpp
QodeAssistClient.hpp QodeAssistClient.cpp
QodeAssistUtils.hpp
DocumentContextReader.hpp DocumentContextReader.cpp
QodeAssistData.hpp
utils/CounterTooltip.hpp utils/CounterTooltip.cpp
settings/GeneralSettings.hpp settings/GeneralSettings.cpp
settings/ContextSettings.hpp settings/ContextSettings.cpp
settings/CustomPromptSettings.hpp settings/CustomPromptSettings.cpp
settings/PresetPromptsSettings.hpp settings/PresetPromptsSettings.cpp
settings/SettingsUtils.hpp
core/ChangesManager.h core/ChangesManager.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
widgets/ContextExtractor.hpp
widgets/DiffStatistics.hpp
QuickRefactorHandler.hpp QuickRefactorHandler.cpp
tools/ToolsRegistration.hpp tools/ToolsRegistration.cpp
tools/ListProjectFilesTool.hpp tools/ListProjectFilesTool.cpp
tools/GetIssuesListTool.hpp tools/GetIssuesListTool.cpp
tools/CreateNewFileTool.hpp tools/CreateNewFileTool.cpp
tools/EditFileTool.hpp tools/EditFileTool.cpp
tools/BuildProjectTool.hpp tools/BuildProjectTool.cpp
tools/ExecuteTerminalCommandTool.hpp tools/ExecuteTerminalCommandTool.cpp
tools/ProjectSearchTool.hpp tools/ProjectSearchTool.cpp
tools/FindFileTool.hpp tools/FindFileTool.cpp
tools/ReadFileTool.hpp tools/ReadFileTool.cpp
tools/FileSearchUtils.hpp tools/FileSearchUtils.cpp
tools/TodoTool.hpp tools/TodoTool.cpp
mcp/McpServerManager.hpp mcp/McpServerManager.cpp
mcp/McpServerConnection.hpp mcp/McpServerConnection.cpp
mcp/McpClientsManager.hpp mcp/McpClientsManager.cpp
settings/McpClientsListAspect.hpp settings/McpClientsListAspect.cpp
)
get_target_property(QtCreatorCorePath QtCreator::Core LOCATION)
find_program(QtCreatorExecutable
NAMES
qtcreator "Qt Creator"
PATHS
"${QtCreatorCorePath}/../../../bin"
"${QtCreatorCorePath}/../../../MacOS"
NO_DEFAULT_PATH
)
if (QtCreatorExecutable)
add_custom_target(RunQtCreator
COMMAND ${QtCreatorExecutable} -pluginpath $<TARGET_FILE_DIR:QodeAssist>
DEPENDS QodeAssist
)
set_target_properties(RunQtCreator PROPERTIES FOLDER "qtc_runnable")
endif()
#TODO change to TS_OUTPUT_DIRECTORY after removing Qt6.8
qt_add_translations(TARGETS QodeAssist
TS_FILE_DIR ${CMAKE_CURRENT_LIST_DIR}/resources/translations
RESOURCE_PREFIX "/translations"
LUPDATE_OPTIONS -no-obsolete
)

93
ChatView/CMakeLists.txt Normal file
View File

@@ -0,0 +1,93 @@
qt_add_library(QodeAssistChatView STATIC)
qt_policy(SET QTP0001 NEW)
qt_policy(SET QTP0004 NEW)
qt_add_qml_module(QodeAssistChatView
URI ChatView
VERSION 1.0
DEPENDENCIES
QtQuick
QML_FILES
qml/RootItem.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/FileMentionPopup.qml
qml/controls/FileEditsActionBar.qml
qml/controls/ContextViewer.qml
qml/controls/Toast.qml
qml/controls/TopBar.qml
qml/controls/SplitDropZone.qml
RESOURCES
icons/attach-file-light.svg
icons/attach-file-dark.svg
icons/close-dark.svg
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/context-icon.svg
icons/open-in-editor.svg
icons/apply-changes-button.svg
icons/undo-changes-button.svg
icons/reject-changes-button.svg
icons/thinking-icon-on.svg
icons/thinking-icon-off.svg
icons/tools-icon-on.svg
icons/tools-icon-off.svg
icons/settings-icon.svg
icons/compress-icon.svg
SOURCES
ChatWidget.hpp ChatWidget.cpp
ChatModel.hpp ChatModel.cpp
ChatRootView.hpp ChatRootView.cpp
ClientInterface.hpp ClientInterface.cpp
MessagePart.hpp
ChatUtils.h ChatUtils.cpp
ChatSerializer.hpp ChatSerializer.cpp
ChatView.hpp ChatView.cpp
ChatData.hpp
FileItem.hpp FileItem.cpp
ChatFileManager.hpp ChatFileManager.cpp
ChatCompressor.hpp ChatCompressor.cpp
FileMentionItem.hpp FileMentionItem.cpp
)
target_link_libraries(QodeAssistChatView
PUBLIC
Qt::Widgets
Qt::Quick
Qt::QuickWidgets
Qt::Network
QtCreator::Core
QtCreator::Utils
PluginLLMCore
QodeAssistSettings
Context
QodeAssistUIControlsplugin
QodeAssistLogger
LLMQore
)
target_include_directories(QodeAssistChatView
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
)

290
ChatView/ChatCompressor.cpp Normal file
View File

@@ -0,0 +1,290 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatCompressor.hpp"
#include <LLMQore/BaseClient.hpp>
#include "ChatModel.hpp"
#include "GeneralSettings.hpp"
#include "PromptTemplateManager.hpp"
#include "ProvidersManager.hpp"
#include "logger/Logger.hpp"
#include <QDateTime>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
namespace QodeAssist::Chat {
ChatCompressor::ChatCompressor(QObject *parent)
: QObject(parent)
{}
void ChatCompressor::startCompression(const QString &chatFilePath, ChatModel *chatModel)
{
if (m_isCompressing) {
emit compressionFailed(tr("Compression already in progress"));
return;
}
if (chatFilePath.isEmpty()) {
emit compressionFailed(tr("No chat file to compress"));
return;
}
if (!chatModel || chatModel->rowCount() == 0) {
emit compressionFailed(tr("Chat is empty, nothing to compress"));
return;
}
auto providerName = Settings::generalSettings().caProvider();
m_provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!m_provider) {
emit compressionFailed(tr("No provider available"));
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = PluginLLMCore::PromptTemplateManager::instance().getChatTemplateByName(
templateName);
if (!promptTemplate) {
emit compressionFailed(tr("No template available"));
return;
}
m_isCompressing = true;
m_chatModel = chatModel;
m_originalChatPath = chatFilePath;
m_accumulatedSummary.clear();
emit compressionStarted();
connectProviderSignals();
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
buildRequestPayload(payload, promptTemplate);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
m_currentRequestId = m_provider->sendRequest(
QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
LOG_MESSAGE(QString("Starting compression request: %1").arg(m_currentRequestId));
}
bool ChatCompressor::isCompressing() const
{
return m_isCompressing;
}
void ChatCompressor::cancelCompression()
{
if (!m_isCompressing)
return;
LOG_MESSAGE("Cancelling compression request");
if (m_provider && !m_currentRequestId.isEmpty())
m_provider->cancelRequest(m_currentRequestId);
cleanupState();
emit compressionFailed(tr("Compression cancelled"));
}
void ChatCompressor::onPartialResponseReceived(const QString &requestId, const QString &partialText)
{
if (!m_isCompressing || requestId != m_currentRequestId)
return;
m_accumulatedSummary += partialText;
}
void ChatCompressor::onFullResponseReceived(const QString &requestId, const QString &fullText)
{
Q_UNUSED(fullText)
if (!m_isCompressing || requestId != m_currentRequestId)
return;
LOG_MESSAGE(
QString("Received summary, length: %1 characters").arg(m_accumulatedSummary.length()));
QString compressedPath = createCompressedChatPath(m_originalChatPath);
if (!createCompressedChatFile(m_originalChatPath, compressedPath, m_accumulatedSummary)) {
handleCompressionError(tr("Failed to save compressed chat"));
return;
}
LOG_MESSAGE(QString("Compression completed: %1").arg(compressedPath));
cleanupState();
emit compressionCompleted(compressedPath);
}
void ChatCompressor::onRequestFailed(const QString &requestId, const QString &error)
{
if (!m_isCompressing || requestId != m_currentRequestId)
return;
LOG_MESSAGE(QString("Compression request failed: %1").arg(error));
handleCompressionError(tr("Compression failed: %1").arg(error));
}
void ChatCompressor::handleCompressionError(const QString &error)
{
cleanupState();
emit compressionFailed(error);
}
QString ChatCompressor::createCompressedChatPath(const QString &originalPath) const
{
QFileInfo fileInfo(originalPath);
QString hash = QString::number(QDateTime::currentMSecsSinceEpoch() % 100000, 16);
return QString("%1/%2_%3.%4")
.arg(fileInfo.absolutePath(), fileInfo.completeBaseName(), hash, fileInfo.suffix());
}
QString ChatCompressor::buildCompressionPrompt() const
{
return QStringLiteral(
"Please create a comprehensive summary of our entire conversation above. "
"The summary should:\n"
"1. Preserve all important context, decisions, and key information\n"
"2. Maintain technical details, code snippets, file references, and specific examples\n"
"3. Keep the chronological flow of the discussion\n"
"4. Be significantly shorter than the original (aim for 30-40% of original length)\n"
"5. Be written in clear, structured format\n"
"6. Use markdown formatting for better readability\n\n"
"Create the summary now:");
}
void ChatCompressor::buildRequestPayload(
QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate)
{
PluginLLMCore::ContextData context;
context.systemPrompt = QStringLiteral(
"You are a helpful assistant that creates concise summaries of conversations. "
"Your summaries preserve key information, technical details, and the flow of discussion.");
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool
|| msg.role == ChatModel::ChatRole::FileEdit
|| msg.role == ChatModel::ChatRole::Thinking)
continue;
PluginLLMCore::Message apiMessage;
apiMessage.role = (msg.role == ChatModel::ChatRole::User) ? "user" : "assistant";
apiMessage.content = msg.content;
messages.append(apiMessage);
}
PluginLLMCore::Message compressionRequest;
compressionRequest.role = "user";
compressionRequest.content = buildCompressionPrompt();
messages.append(compressionRequest);
context.history = messages;
m_provider->prepareRequest(
payload, promptTemplate, context, PluginLLMCore::RequestType::Chat, false, false);
}
bool ChatCompressor::createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary)
{
QFile sourceFile(sourcePath);
if (!sourceFile.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open source chat file: %1").arg(sourcePath));
return false;
}
QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(sourceFile.readAll(), &parseError);
sourceFile.close();
if (doc.isNull() || !doc.isObject()) {
LOG_MESSAGE(QString("Invalid JSON in chat file: %1 (Error: %2)")
.arg(sourcePath, parseError.errorString()));
return false;
}
QJsonObject root = doc.object();
QJsonObject summaryMessage;
summaryMessage["role"] = "assistant";
summaryMessage["content"] = QString("# Chat Summary\n\n%1").arg(summary);
summaryMessage["id"] = QUuid::createUuid().toString(QUuid::WithoutBraces);
summaryMessage["isRedacted"] = false;
summaryMessage["attachments"] = QJsonArray();
summaryMessage["images"] = QJsonArray();
root["messages"] = QJsonArray{summaryMessage};
if (QFile::exists(destPath))
QFile::remove(destPath);
QFile destFile(destPath);
if (!destFile.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to create compressed chat file: %1").arg(destPath));
return false;
}
destFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented));
return true;
}
void ChatCompressor::connectProviderSignals()
{
auto *c = m_provider->client();
m_connections.append(connect(
c,
&::LLMQore::BaseClient::chunkReceived,
this,
&ChatCompressor::onPartialResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestCompleted,
this,
&ChatCompressor::onFullResponseReceived,
Qt::UniqueConnection));
m_connections.append(connect(
c,
&::LLMQore::BaseClient::requestFailed,
this,
&ChatCompressor::onRequestFailed,
Qt::UniqueConnection));
}
void ChatCompressor::disconnectAllSignals()
{
for (const auto &connection : std::as_const(m_connections))
disconnect(connection);
m_connections.clear();
}
void ChatCompressor::cleanupState()
{
disconnectAllSignals();
m_isCompressing = false;
m_currentRequestId.clear();
m_originalChatPath.clear();
m_accumulatedSummary.clear();
m_chatModel = nullptr;
m_provider = nullptr;
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonObject>
#include <QList>
#include <QObject>
#include <QString>
namespace QodeAssist::PluginLLMCore {
class Provider;
class PromptTemplate;
} // namespace QodeAssist::PluginLLMCore
namespace QodeAssist::Chat {
class ChatModel;
class ChatCompressor : public QObject
{
Q_OBJECT
public:
explicit ChatCompressor(QObject *parent = nullptr);
void startCompression(const QString &chatFilePath, ChatModel *chatModel);
bool isCompressing() const;
void cancelCompression();
signals:
void compressionStarted();
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
private slots:
void onPartialResponseReceived(const QString &requestId, const QString &partialText);
void onFullResponseReceived(const QString &requestId, const QString &fullText);
void onRequestFailed(const QString &requestId, const QString &error);
private:
QString createCompressedChatPath(const QString &originalPath) const;
QString buildCompressionPrompt() const;
bool createCompressedChatFile(
const QString &sourcePath, const QString &destPath, const QString &summary);
void connectProviderSignals();
void disconnectAllSignals();
void cleanupState();
void handleCompressionError(const QString &error);
void buildRequestPayload(QJsonObject &payload, PluginLLMCore::PromptTemplate *promptTemplate);
bool m_isCompressing = false;
QString m_currentRequestId;
QString m_originalChatPath;
QString m_accumulatedSummary;
PluginLLMCore::Provider *m_provider = nullptr;
ChatModel *m_chatModel = nullptr;
QList<QMetaObject::Connection> m_connections;
};
} // namespace QodeAssist::Chat

16
ChatView/ChatData.hpp Normal file
View File

@@ -0,0 +1,16 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QtQmlIntegration>
namespace QodeAssist::Chat {
Q_NAMESPACE
QML_NAMED_ELEMENT(MessagePartType)
enum class MessagePartType { Code, Text, Image };
Q_ENUM_NS(MessagePartType)
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,190 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatFileManager.hpp"
#include "Logger.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QStandardPaths>
#include <QUuid>
#include <QDateTime>
#include <QRegularExpression>
#include <coreplugin/icore.h>
namespace QodeAssist::Chat {
ChatFileManager::ChatFileManager(QObject *parent)
: QObject(parent)
, m_intermediateStorageDir(getIntermediateStorageDir())
{}
ChatFileManager::~ChatFileManager() = default;
QStringList ChatFileManager::processDroppedFiles(const QStringList &filePaths)
{
QStringList processedPaths;
processedPaths.reserve(filePaths.size());
for (const QString &filePath : filePaths) {
if (!isFileAccessible(filePath)) {
const QString error = tr("File is not accessible: %1").arg(filePath);
LOG_MESSAGE(error);
emit fileOperationFailed(error);
continue;
}
QString copiedPath = copyToIntermediateStorage(filePath);
if (!copiedPath.isEmpty()) {
processedPaths.append(copiedPath);
emit fileCopiedToStorage(filePath, copiedPath);
LOG_MESSAGE(QString("File copied to storage: %1 -> %2").arg(filePath, copiedPath));
} else {
const QString error = tr("Failed to copy file: %1").arg(filePath);
LOG_MESSAGE(error);
emit fileOperationFailed(error);
}
}
return processedPaths;
}
void ChatFileManager::setChatFilePath(const QString &chatFilePath)
{
m_chatFilePath = chatFilePath;
}
QString ChatFileManager::chatFilePath() const
{
return m_chatFilePath;
}
void ChatFileManager::clearIntermediateStorage()
{
QDir dir(m_intermediateStorageDir);
if (!dir.exists()) {
return;
}
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
for (const QFileInfo &fileInfo : files) {
QFile file(fileInfo.absoluteFilePath());
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
if (file.remove()) {
LOG_MESSAGE(QString("Removed intermediate file: %1").arg(fileInfo.fileName()));
} else {
LOG_MESSAGE(QString("Failed to remove intermediate file: %1")
.arg(fileInfo.fileName()));
}
}
}
bool ChatFileManager::isFileAccessible(const QString &filePath)
{
QFileInfo fileInfo(filePath);
return fileInfo.exists() && fileInfo.isFile() && fileInfo.isReadable();
}
void ChatFileManager::cleanupGlobalIntermediateStorage()
{
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
QDir dir(intermediatePath);
if (!dir.exists()) {
return;
}
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot);
int removedCount = 0;
int failedCount = 0;
for (const QFileInfo &fileInfo : files) {
QFile file(fileInfo.absoluteFilePath());
file.setPermissions(QFile::WriteUser | QFile::ReadUser);
if (file.remove()) {
removedCount++;
} else {
failedCount++;
}
}
if (removedCount > 0 || failedCount > 0) {
LOG_MESSAGE(QString("ChatFileManager global cleanup: removed=%1, failed=%2")
.arg(removedCount)
.arg(failedCount));
}
}
QString ChatFileManager::copyToIntermediateStorage(const QString &filePath)
{
QFileInfo fileInfo(filePath);
if (!fileInfo.exists() || !fileInfo.isFile()) {
LOG_MESSAGE(QString("Source file does not exist or is not a file: %1").arg(filePath));
return QString();
}
if (fileInfo.size() == 0) {
LOG_MESSAGE(QString("Source file is empty: %1").arg(filePath));
}
const QString newFileName = generateIntermediateFileName(filePath);
const QString destinationPath = QDir(m_intermediateStorageDir).filePath(newFileName);
if (QFileInfo::exists(destinationPath)) {
QFile::remove(destinationPath);
}
if (!QFile::copy(filePath, destinationPath)) {
LOG_MESSAGE(QString("Failed to copy file: %1 -> %2").arg(filePath, destinationPath));
return QString();
}
QFile copiedFile(destinationPath);
if (!copiedFile.exists()) {
LOG_MESSAGE(QString("Copied file does not exist after copy: %1").arg(destinationPath));
return QString();
}
copiedFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
return destinationPath;
}
QString ChatFileManager::getIntermediateStorageDir()
{
const QString basePath = Core::ICore::userResourcePath().toFSPathString();
const QString intermediatePath = QDir(basePath).filePath("qodeassist/chat_temp_files");
QDir dir;
if (!dir.exists(intermediatePath) && !dir.mkpath(intermediatePath)) {
LOG_MESSAGE(QString("Failed to create intermediate storage directory: %1")
.arg(intermediatePath));
}
return intermediatePath;
}
QString ChatFileManager::generateIntermediateFileName(const QString &originalPath)
{
const QFileInfo fileInfo(originalPath);
const QString extension = fileInfo.suffix();
QString baseName = fileInfo.completeBaseName().left(30);
static const QRegularExpression specialChars("[^a-zA-Z0-9_-]");
baseName.replace(specialChars, "_");
if (baseName.isEmpty()) {
baseName = "file";
}
const QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss");
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces).left(8);
return QString("%1_%2_%3.%4").arg(baseName, timestamp, uuid, extension);
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QString>
#include <QStringList>
#include <QMap>
namespace QodeAssist::Chat {
class ChatFileManager : public QObject
{
Q_OBJECT
public:
explicit ChatFileManager(QObject *parent = nullptr);
~ChatFileManager();
QStringList processDroppedFiles(const QStringList &filePaths);
void setChatFilePath(const QString &chatFilePath);
QString chatFilePath() const;
void clearIntermediateStorage();
static bool isFileAccessible(const QString &filePath);
static void cleanupGlobalIntermediateStorage();
signals:
void fileOperationFailed(const QString &error);
void fileCopiedToStorage(const QString &originalPath, const QString &newPath);
private:
QString copyToIntermediateStorage(const QString &filePath);
QString getIntermediateStorageDir();
QString generateIntermediateFileName(const QString &originalPath);
QString m_chatFilePath;
QString m_intermediateStorageDir;
};
} // namespace QodeAssist::Chat

585
ChatView/ChatModel.cpp Normal file
View File

@@ -0,0 +1,585 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatModel.hpp"
#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 {
ChatModel::ChatModel(QObject *parent)
: QAbstractListModel(parent)
{
auto &settings = Settings::chatAssistantSettings();
connect(
&settings.chatTokensThreshold,
&Utils::BaseAspect::changed,
this,
&ChatModel::tokensThresholdChanged);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditApplied,
this,
&ChatModel::onFileEditApplied);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditRejected,
this,
&ChatModel::onFileEditRejected);
connect(&Context::ChangesManager::instance(),
&Context::ChangesManager::fileEditArchived,
this,
&ChatModel::onFileEditArchived);
}
int ChatModel::rowCount(const QModelIndex &parent) const
{
return m_messages.size();
}
QVariant ChatModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_messages.size())
return QVariant();
const Message &message = m_messages[index.row()];
switch (static_cast<Roles>(role)) {
case Roles::RoleType:
return QVariant::fromValue(message.role);
case Roles::Content: {
return message.content;
}
case Roles::Attachments: {
QVariantList attachmentsList;
for (const auto &attachment : message.attachments) {
QVariantMap attachmentMap;
attachmentMap["fileName"] = attachment.filename;
attachmentMap["storedPath"] = attachment.content;
if (!m_chatFilePath.isEmpty()) {
QFileInfo fileInfo(m_chatFilePath);
QString baseName = fileInfo.completeBaseName();
QString dirPath = fileInfo.absolutePath();
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
QString fullPath = QDir(contentFolder).filePath(attachment.content);
attachmentMap["filePath"] = fullPath;
} else {
attachmentMap["filePath"] = QString();
}
attachmentsList.append(attachmentMap);
}
return attachmentsList;
}
case Roles::IsRedacted: {
return message.isRedacted;
}
case Roles::Images: {
QVariantList imagesList;
for (const auto &image : message.images) {
QVariantMap imageMap;
imageMap["fileName"] = image.fileName;
imageMap["storedPath"] = image.storedPath;
imageMap["mediaType"] = image.mediaType;
if (!m_chatFilePath.isEmpty()) {
QFileInfo fileInfo(m_chatFilePath);
QString baseName = fileInfo.completeBaseName();
QString dirPath = fileInfo.absolutePath();
QString contentFolder = QDir(dirPath).filePath(baseName + "_content");
QString fullPath = QDir(contentFolder).filePath(image.storedPath);
imageMap["imageUrl"] = QUrl::fromLocalFile(fullPath).toString();
imageMap["filePath"] = fullPath;
} else {
imageMap["imageUrl"] = QString();
imageMap["filePath"] = QString();
}
imagesList.append(imageMap);
}
return imagesList;
}
default:
return QVariant();
}
}
QHash<int, QByteArray> ChatModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[Roles::RoleType] = "roleType";
roles[Roles::Content] = "content";
roles[Roles::Attachments] = "attachments";
roles[Roles::IsRedacted] = "isRedacted";
roles[Roles::Images] = "images";
return roles;
}
void ChatModel::addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments,
const QList<ImageAttachment> &images,
bool isRedacted,
const QString &signature)
{
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;
lastMessage.isRedacted = isRedacted;
lastMessage.signature = signature;
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;
newMessage.isRedacted = isRedacted;
newMessage.signature = signature;
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));
}
}
}
}
}
}
}
QVector<ChatModel::Message> ChatModel::getChatHistory() const
{
return m_messages;
}
void ChatModel::clear()
{
beginResetModel();
m_messages.clear();
endResetModel();
emit modelReseted();
}
QList<MessagePart> ChatModel::processMessageContent(const QString &content) const
{
QList<MessagePart> parts;
QRegularExpression codeBlockRegex("```(\\w*)\\n?([\\s\\S]*?)```");
int lastIndex = 0;
auto blockMatches = codeBlockRegex.globalMatch(content);
while (blockMatches.hasNext()) {
auto match = blockMatches.next();
if (match.capturedStart() > lastIndex) {
QString textBetween
= content.mid(lastIndex, match.capturedStart() - lastIndex).trimmed();
if (!textBetween.isEmpty()) {
MessagePart part;
part.type = MessagePartType::Text;
part.text = textBetween;
parts.append(part);
}
}
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();
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);
}
}
return parts;
}
QJsonArray ChatModel::prepareMessagesForRequest(const QString &systemPrompt) const
{
QJsonArray messages;
messages.append(QJsonObject{{"role", "system"}, {"content", systemPrompt}});
for (const auto &message : m_messages) {
QString role;
switch (message.role) {
case ChatRole::User:
role = "user";
break;
case ChatRole::Assistant:
role = "assistant";
break;
case ChatRole::Tool:
case ChatRole::FileEdit:
continue;
default:
continue;
}
QString content
= message.attachments.isEmpty()
? message.content
: message.content + "\n\nAttached files list:"
+ std::accumulate(
message.attachments.begin(),
message.attachments.end(),
QString(),
[](QString acc, const Context::ContentFile &attachment) {
return acc
+ QString("\nname: %1\nfile content:\n%2")
.arg(attachment.filename, attachment.content);
});
messages.append(QJsonObject{{"role", role}, {"content", content}});
}
return messages;
}
int ChatModel::tokensThreshold() const
{
auto &settings = Settings::chatAssistantSettings();
return settings.chatTokensThreshold();
}
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) + "...]";
}
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::Thinking && m_messages[i].id == requestId) {
m_messages[i].content = displayContent;
m_messages[i].signature = signature;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated existing thinking message at index %1").arg(i));
return;
}
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = false;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::addRedactedThinkingBlock(const QString &requestId, const QString &signature)
{
LOG_MESSAGE(
QString("Adding redacted thinking block: requestId=%1, signature length=%2")
.arg(requestId)
.arg(signature.length()));
QString displayContent = "[Thinking content redacted by safety systems]";
if (!signature.isEmpty()) {
displayContent += "\n[Signature: " + signature.left(40) + "...]";
}
beginInsertRows(QModelIndex(), m_messages.size(), m_messages.size());
Message thinkingMessage;
thinkingMessage.role = ChatRole::Thinking;
thinkingMessage.content = displayContent;
thinkingMessage.id = requestId;
thinkingMessage.isRedacted = true;
thinkingMessage.signature = signature;
m_messages.append(thinkingMessage);
endInsertRows();
LOG_MESSAGE(QString("Added redacted thinking message at index %1 with signature length=%2")
.arg(m_messages.size() - 1).arg(signature.length()));
}
void ChatModel::updateMessageContent(const QString &messageId, const QString &newContent)
{
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].id == messageId) {
m_messages[i].content = newContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated message content for id: %1").arg(messageId));
break;
}
}
}
void ChatModel::setLoadingFromHistory(bool loading)
{
m_loadingFromHistory = loading;
LOG_MESSAGE(QString("ChatModel loading from history: %1").arg(loading ? "true" : "false"));
}
bool ChatModel::isLoadingFromHistory() const
{
return m_loadingFromHistory;
}
void ChatModel::onFileEditApplied(const QString &editId)
{
updateFileEditStatus(editId, "applied", "Successfully applied");
}
void ChatModel::onFileEditRejected(const QString &editId)
{
updateFileEditStatus(editId, "rejected", "Rejected by user");
}
void ChatModel::onFileEditArchived(const QString &editId)
{
updateFileEditStatus(editId, "archived", "Archived (from previous conversation turn)");
}
void ChatModel::updateFileEditStatus(const QString &editId, const QString &status, const QString &statusMessage)
{
const QString marker = "QODEASSIST_FILE_EDIT:";
for (int i = 0; i < m_messages.size(); ++i) {
if (m_messages[i].role == ChatRole::FileEdit && m_messages[i].id == editId) {
const QString &content = m_messages[i].content;
if (content.contains(marker)) {
int markerPos = content.indexOf(marker);
int jsonStart = markerPos + marker.length();
if (jsonStart < content.length()) {
QString jsonStr = content.mid(jsonStart);
QJsonDocument doc = QJsonDocument::fromJson(jsonStr.toUtf8());
if (doc.isObject()) {
QJsonObject editData = doc.object();
editData["status"] = status;
editData["status_message"] = statusMessage;
QString updatedContent = marker
+ QString::fromUtf8(QJsonDocument(editData).toJson(QJsonDocument::Compact));
m_messages[i].content = updatedContent;
emit dataChanged(index(i), index(i));
LOG_MESSAGE(QString("Updated FileEdit message status: editId=%1, status=%2")
.arg(editId, status));
break;
}
}
}
}
}
}
void ChatModel::setChatFilePath(const QString &filePath)
{
m_chatFilePath = filePath;
}
QString ChatModel::chatFilePath() const
{
return m_chatFilePath;
}
} // namespace QodeAssist::Chat

113
ChatView/ChatModel.hpp Normal file
View File

@@ -0,0 +1,113 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "ContextData.hpp"
#include "MessagePart.hpp"
#include <QAbstractListModel>
#include <QJsonArray>
#include <QtQmlIntegration>
#include "context/ContentFile.hpp"
namespace QodeAssist::Chat {
class ChatModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int tokensThreshold READ tokensThreshold NOTIFY tokensThresholdChanged FINAL)
QML_ELEMENT
public:
enum ChatRole { System, User, Assistant, Tool, FileEdit, Thinking };
Q_ENUM(ChatRole)
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);
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;
Q_INVOKABLE void addMessage(
const QString &content,
ChatRole role,
const QString &id,
const QList<Context::ContentFile> &attachments = {},
const QList<ImageAttachment> &images = {},
bool isRedacted = false,
const QString &signature = QString());
Q_INVOKABLE void clear();
Q_INVOKABLE QList<MessagePart> processMessageContent(const QString &content) const;
QVector<Message> getChatHistory() const;
QJsonArray prepareMessagesForRequest(const QString &systemPrompt) const;
int tokensThreshold() const;
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
Q_DECLARE_METATYPE(QodeAssist::Chat::ChatModel::Message)
Q_DECLARE_METATYPE(QodeAssist::Chat::MessagePart)

1535
ChatView/ChatRootView.cpp Normal file

File diff suppressed because it is too large Load Diff

253
ChatView/ChatRootView.hpp Normal file
View File

@@ -0,0 +1,253 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QQuickItem>
#include <QVariantList>
#include "ChatFileManager.hpp"
#include "ChatModel.hpp"
#include "ClientInterface.hpp"
#include "pluginllmcore/PromptProviderChat.hpp"
#include <coreplugin/editormanager/editormanager.h>
namespace QodeAssist::Chat {
class ChatCompressor;
class ChatRootView : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QodeAssist::Chat::ChatModel *chatModel READ chatModel NOTIFY chatModelChanged FINAL)
Q_PROPERTY(QString currentTemplate READ currentTemplate NOTIFY currentTemplateChanged FINAL)
Q_PROPERTY(bool isSyncOpenFiles READ isSyncOpenFiles NOTIFY isSyncOpenFilesChanged FINAL)
Q_PROPERTY(QStringList attachmentFiles READ attachmentFiles NOTIFY attachmentFilesChanged FINAL)
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 useTools READ useTools WRITE setUseTools NOTIFY useToolsChanged FINAL)
Q_PROPERTY(bool useThinking READ useThinking WRITE setUseThinking NOTIFY useThinkingChanged FINAL)
Q_PROPERTY(int currentMessageTotalEdits READ currentMessageTotalEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageAppliedEdits READ currentMessageAppliedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessagePendingEdits READ currentMessagePendingEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(int currentMessageRejectedEdits READ currentMessageRejectedEdits NOTIFY currentMessageEditsStatsChanged FINAL)
Q_PROPERTY(bool isThinkingSupport READ isThinkingSupport NOTIFY isThinkingSupportChanged FINAL)
Q_PROPERTY(QStringList availableConfigurations READ availableConfigurations NOTIFY availableConfigurationsChanged FINAL)
Q_PROPERTY(QString currentConfiguration READ currentConfiguration NOTIFY currentConfigurationChanged FINAL)
Q_PROPERTY(QStringList availableAgentRoles READ availableAgentRoles NOTIFY availableAgentRolesChanged FINAL)
Q_PROPERTY(QString currentAgentRole READ currentAgentRole NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString baseSystemPrompt READ baseSystemPrompt NOTIFY baseSystemPromptChanged FINAL)
Q_PROPERTY(QString currentAgentRoleDescription READ currentAgentRoleDescription NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(QString currentAgentRoleSystemPrompt READ currentAgentRoleSystemPrompt NOTIFY currentAgentRoleChanged FINAL)
Q_PROPERTY(bool isCompressing READ isCompressing NOTIFY isCompressingChanged FINAL)
QML_ELEMENT
public:
ChatRootView(QQuickItem *parent = nullptr);
ChatModel *chatModel() const;
QString currentTemplate() const;
void saveHistory(const QString &filePath);
void loadHistory(const QString &filePath);
Q_INVOKABLE void showSaveDialog();
Q_INVOKABLE void showLoadDialog();
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 openSettings();
Q_INVOKABLE void openFileInEditor(const QString &filePath);
Q_INVOKABLE void updateInputTokensCount();
int inputTokensCount() const;
bool isSyncOpenFiles() const;
void onEditorAboutToClose(Core::IEditor *editor);
void onAppendLinkFileFromEditor(Core::IEditor *editor);
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 useTools() const;
void setUseTools(bool enabled);
bool useThinking() const;
void setUseThinking(bool enabled);
Q_INVOKABLE void applyFileEdit(const QString &editId);
Q_INVOKABLE void rejectFileEdit(const QString &editId);
Q_INVOKABLE void undoFileEdit(const QString &editId);
Q_INVOKABLE void openFileEditInEditor(const QString &editId);
Q_INVOKABLE void applyAllFileEditsForCurrentMessage();
Q_INVOKABLE void undoAllFileEditsForCurrentMessage();
Q_INVOKABLE void updateCurrentMessageEditsStats();
Q_INVOKABLE void loadAvailableConfigurations();
Q_INVOKABLE void applyConfiguration(const QString &configName);
QStringList availableConfigurations() const;
QString currentConfiguration() const;
Q_INVOKABLE void compressCurrentChat();
Q_INVOKABLE void cancelCompression();
Q_INVOKABLE void loadAvailableAgentRoles();
Q_INVOKABLE void applyAgentRole(const QString &roleId);
Q_INVOKABLE void openAgentRolesSettings();
QStringList availableAgentRoles() const;
QString currentAgentRole() const;
QString baseSystemPrompt() const;
QString currentAgentRoleDescription() const;
QString currentAgentRoleSystemPrompt() const;
int currentMessageTotalEdits() const;
int currentMessageAppliedEdits() const;
int currentMessagePendingEdits() const;
int currentMessageRejectedEdits() const;
QString lastInfoMessage() const;
bool isThinkingSupport() const;
bool isCompressing() const;
public slots:
void sendMessage(const QString &message);
void copyToClipboard(const QString &text);
void cancelRequest();
void clearAttachmentFiles();
void clearLinkedFiles();
void clearMessages();
signals:
void chatModelChanged();
void currentTemplateChanged();
void attachmentFilesChanged();
void linkedFilesChanged();
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 useToolsChanged();
void useThinkingChanged();
void currentMessageEditsStatsChanged();
void isThinkingSupportChanged();
void availableConfigurationsChanged();
void currentConfigurationChanged();
void availableAgentRolesChanged();
void currentAgentRoleChanged();
void baseSystemPromptChanged();
void isCompressingChanged();
void compressionCompleted(const QString &compressedChatPath);
void compressionFailed(const QString &error);
void openFilesChanged();
private:
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;
PluginLLMCore::PromptProviderChat m_promptProvider;
ClientInterface *m_clientInterface;
ChatFileManager *m_fileManager;
QString m_currentTemplate;
QString m_recentFilePath;
QStringList m_attachmentFiles;
QStringList m_linkedFiles;
int m_messageTokensCount{0};
int m_inputTokensCount{0};
bool m_isSyncOpenFiles;
QList<Core::IEditor *> m_currentEditors;
bool m_isRequestInProgress;
QString m_lastErrorMessage;
QVariantList m_activeRules;
QString m_currentMessageRequestId;
int m_currentMessageTotalEdits{0};
int m_currentMessageAppliedEdits{0};
int m_currentMessagePendingEdits{0};
int m_currentMessageRejectedEdits{0};
QString m_lastInfoMessage;
QStringList m_availableConfigurations;
QString m_currentConfiguration;
QStringList m_availableAgentRoles;
QString m_currentAgentRole;
ChatCompressor *m_chatCompressor;
};
} // namespace QodeAssist::Chat

287
ChatView/ChatSerializer.cpp Normal file
View File

@@ -0,0 +1,287 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatSerializer.hpp"
#include "Logger.hpp"
#include <QBuffer>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QJsonArray>
#include <QJsonDocument>
#include <QUuid>
namespace QodeAssist::Chat {
const QString ChatSerializer::VERSION = "0.2";
SerializationResult ChatSerializer::saveToFile(const ChatModel *model, const QString &filePath)
{
if (!ensureDirectoryExists(filePath)) {
return {false, "Failed to create directory structure"};
}
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly)) {
return {false, QString("Failed to open file for writing: %1").arg(filePath)};
}
QJsonObject root = serializeChat(model, filePath);
QJsonDocument doc(root);
if (file.write(doc.toJson(QJsonDocument::Indented)) == -1) {
return {false, QString("Failed to write to file: %1").arg(file.errorString())};
}
return {true, QString()};
}
SerializationResult ChatSerializer::loadFromFile(ChatModel *model, const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
return {false, QString("Failed to open file for reading: %1").arg(filePath)};
}
QJsonParseError error;
QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
return {false, QString("JSON parse error: %1").arg(error.errorString())};
}
QJsonObject root = doc.object();
QString version = root["version"].toString();
if (!validateVersion(version)) {
return {false, QString("Unsupported version: %1").arg(version)};
}
if (!deserializeChat(model, root, filePath)) {
return {false, "Failed to deserialize chat data"};
}
return {true, QString()};
}
QJsonObject ChatSerializer::serializeMessage(
const ChatModel::Message &message, const QString &chatFilePath)
{
QJsonObject messageObj;
messageObj["role"] = static_cast<int>(message.role);
messageObj["content"] = message.content;
messageObj["id"] = message.id;
if (message.isRedacted) {
messageObj["isRedacted"] = true;
}
if (!message.signature.isEmpty()) {
messageObj["signature"] = message.signature;
}
if (!message.attachments.isEmpty()) {
QJsonArray attachmentsArray;
for (const auto &attachment : message.attachments) {
QJsonObject attachmentObj;
attachmentObj["fileName"] = attachment.filename;
attachmentObj["storedPath"] = attachment.content;
attachmentsArray.append(attachmentObj);
}
messageObj["attachments"] = attachmentsArray;
}
if (!message.images.isEmpty()) {
QJsonArray imagesArray;
for (const auto &image : message.images) {
QJsonObject imageObj;
imageObj["fileName"] = image.fileName;
imageObj["storedPath"] = image.storedPath;
imageObj["mediaType"] = image.mediaType;
imagesArray.append(imageObj);
}
messageObj["images"] = imagesArray;
}
return messageObj;
}
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("attachments")) {
QJsonArray attachmentsArray = json["attachments"].toArray();
for (const auto &attachmentValue : attachmentsArray) {
QJsonObject attachmentObj = attachmentValue.toObject();
Context::ContentFile attachment;
attachment.filename = attachmentObj["fileName"].toString();
attachment.content = attachmentObj["storedPath"].toString();
message.attachments.append(attachment);
}
}
if (json.contains("images")) {
QJsonArray imagesArray = json["images"].toArray();
for (const auto &imageValue : imagesArray) {
QJsonObject imageObj = imageValue.toObject();
ChatModel::ImageAttachment image;
image.fileName = imageObj["fileName"].toString();
image.storedPath = imageObj["storedPath"].toString();
image.mediaType = imageObj["mediaType"].toString();
message.images.append(image);
}
}
return message;
}
QJsonObject ChatSerializer::serializeChat(const ChatModel *model, const QString &chatFilePath)
{
QJsonArray messagesArray;
for (const auto &message : model->getChatHistory()) {
messagesArray.append(serializeMessage(message, chatFilePath));
}
QJsonObject root;
root["version"] = VERSION;
root["messages"] = messagesArray;
return root;
}
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(), chatFilePath));
}
model->clear();
model->setLoadingFromHistory(true);
for (const auto &message : messages) {
model->addMessage(
message.content,
message.role,
message.id,
message.attachments,
message.images,
message.isRedacted,
message.signature);
LOG_MESSAGE(QString("Loaded message with %1 image(s), isRedacted=%2, signature length=%3")
.arg(message.images.size())
.arg(message.isRedacted)
.arg(message.signature.length()));
}
model->setLoadingFromHistory(false);
return true;
}
bool ChatSerializer::ensureDirectoryExists(const QString &filePath)
{
QFileInfo fileInfo(filePath);
QDir dir = fileInfo.dir();
return dir.exists() || dir.mkpath(".");
}
bool ChatSerializer::validateVersion(const QString &version)
{
if (version == VERSION) {
return true;
}
if (version == "0.1") {
LOG_MESSAGE(
"Loading chat from old format 0.1 - images folder structure has changed from _images "
"to _content");
return true;
}
return false;
}
QString ChatSerializer::getChatContentFolder(const QString &chatFilePath)
{
QFileInfo fileInfo(chatFilePath);
QString baseName = fileInfo.completeBaseName();
QString dirPath = fileInfo.absolutePath();
return QDir(dirPath).filePath(baseName + "_content");
}
bool ChatSerializer::saveContentToStorage(
const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath)
{
QString contentFolder = getChatContentFolder(chatFilePath);
QDir dir;
if (!dir.exists(contentFolder)) {
if (!dir.mkpath(contentFolder)) {
LOG_MESSAGE(QString("Failed to create content folder: %1").arg(contentFolder));
return false;
}
}
QFileInfo originalFileInfo(fileName);
QString extension = originalFileInfo.suffix();
QString baseName = originalFileInfo.completeBaseName();
QString uniqueName = QString("%1_%2.%3")
.arg(baseName)
.arg(QUuid::createUuid().toString(QUuid::WithoutBraces).left(8))
.arg(extension);
QString fullPath = QDir(contentFolder).filePath(uniqueName);
QByteArray contentData = QByteArray::fromBase64(base64Data.toUtf8());
QFile file(fullPath);
if (!file.open(QIODevice::WriteOnly)) {
LOG_MESSAGE(QString("Failed to open file for writing: %1").arg(fullPath));
return false;
}
if (file.write(contentData) == -1) {
LOG_MESSAGE(QString("Failed to write content data: %1").arg(file.errorString()));
return false;
}
file.close();
storedPath = uniqueName;
LOG_MESSAGE(QString("Saved content: %1 to %2").arg(fileName, fullPath));
return true;
}
QString ChatSerializer::loadContentFromStorage(const QString &chatFilePath, const QString &storedPath)
{
QString contentFolder = getChatContentFolder(chatFilePath);
QString fullPath = QDir(contentFolder).filePath(storedPath);
QFile file(fullPath);
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open content file: %1").arg(fullPath));
return QString();
}
QByteArray contentData = file.readAll();
file.close();
return contentData.toBase64();
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QJsonArray>
#include <QJsonObject>
#include <QString>
#include "ChatModel.hpp"
namespace QodeAssist::Chat {
struct SerializationResult
{
bool success{false};
QString errorMessage;
};
class ChatSerializer
{
public:
static SerializationResult saveToFile(const ChatModel *model, const QString &filePath);
static SerializationResult loadFromFile(ChatModel *model, const QString &filePath);
// Public for testing purposes
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);
// Content management (images and text files)
static QString getChatContentFolder(const QString &chatFilePath);
static bool saveContentToStorage(const QString &chatFilePath,
const QString &fileName,
const QString &base64Data,
QString &storedPath);
static QString loadContentFromStorage(const QString &chatFilePath, const QString &storedPath);
private:
static const QString VERSION;
static constexpr int CURRENT_VERSION = 1;
static bool ensureDirectoryExists(const QString &filePath);
static bool validateVersion(const QString &version);
};
} // namespace QodeAssist::Chat

52
ChatView/ChatUtils.cpp Normal file
View File

@@ -0,0 +1,52 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatUtils.h"
#include <QClipboard>
#include <QGuiApplication>
namespace QodeAssist::Chat {
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

24
ChatView/ChatUtils.h Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
namespace QodeAssist::Chat {
class ChatUtils : public QObject
{
Q_OBJECT
QML_NAMED_ELEMENT(ChatUtils)
public:
explicit ChatUtils(QObject *parent = nullptr)
: QObject(parent) {};
Q_INVOKABLE void copyToClipboard(const QString &text);
Q_INVOKABLE QString getSafeMarkdownText(const QString &text) const;
};
} // namespace QodeAssist::Chat

90
ChatView/ChatView.cpp Normal file
View File

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

35
ChatView/ChatView.hpp Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QQuickView>
#include <QShortcut>
namespace QodeAssist::Chat {
class ChatView : public QQuickView
{
Q_OBJECT
Q_PROPERTY(bool isPin READ isPin WRITE setIsPin NOTIFY isPinChanged FINAL)
public:
ChatView();
bool isPin() const;
void setIsPin(bool newIsPin);
signals:
void isPinChanged();
protected:
void closeEvent(QCloseEvent *event) override;
private:
void saveSettings();
void restoreSettings();
bool m_isPin;
QShortcut *m_closeShortcut;
};
} // namespace QodeAssist::Chat

27
ChatView/ChatWidget.cpp Normal file
View File

@@ -0,0 +1,27 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ChatWidget.hpp"
#include <QQmlContext>
#include <QQmlEngine>
namespace QodeAssist::Chat {
ChatWidget::ChatWidget(QWidget *parent)
: QQuickWidget(parent)
{
setSource(QUrl("qrc:/qt/qml/ChatView/qml/RootItem.qml"));
setResizeMode(QQuickWidget::SizeRootObjectToView);
}
void ChatWidget::clear()
{
QMetaObject::invokeMethod(rootObject(), "clearChat");
}
void ChatWidget::scrollToBottom()
{
QMetaObject::invokeMethod(rootObject(), "scrollToBottom");
}
} // namespace QodeAssist::Chat

25
ChatView/ChatWidget.hpp Normal file
View File

@@ -0,0 +1,25 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QtQuickWidgets/QtQuickWidgets>
namespace QodeAssist::Chat {
class ChatWidget : public QQuickWidget
{
Q_OBJECT
public:
explicit ChatWidget(QWidget *parent = nullptr);
~ChatWidget() = default;
Q_INVOKABLE void clear();
Q_INVOKABLE void scrollToBottom();
signals:
void clearPressed();
};
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,609 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <projectexplorer/buildconfiguration.h>
#include <projectexplorer/target.h>
#include <texteditor/textdocument.h>
#include <QFile>
#include <QFileInfo>
#include <QImageReader>
#include <QJsonArray>
#include <QJsonDocument>
#include <QMimeDatabase>
#include <QUuid>
#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 <LLMQore/ToolsManager.hpp>
#include "tools/TodoTool.hpp"
#include "ChatAssistantSettings.hpp"
#include "ChatSerializer.hpp"
#include "GeneralSettings.hpp"
#include "Logger.hpp"
#include "ProvidersManager.hpp"
#include "ToolsSettings.hpp"
#include <RulesLoader.hpp>
#include <context/ChangesManager.h>
namespace QodeAssist::Chat {
ClientInterface::ClientInterface(
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent)
: QObject(parent)
, m_chatModel(chatModel)
, m_promptProvider(promptProvider)
, m_contextManager(new Context::ContextManager(this))
{}
ClientInterface::~ClientInterface()
{
cancelRequest();
}
void ClientInterface::sendMessage(
const QString &message,
const QList<QString> &attachments,
const QList<QString> &linkedFiles,
bool useTools,
bool useThinking)
{
cancelRequest();
m_accumulatedResponses.clear();
Context::ChangesManager::instance().archiveAllNonArchivedEdits();
QList<QString> imageFiles;
QList<QString> textFiles;
for (const QString &filePath : attachments) {
if (isImageFile(filePath)) {
imageFiles.append(filePath);
} else {
textFiles.append(filePath);
}
}
QList<Context::ContentFile> storedAttachments;
if (!textFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
auto attachFiles = m_contextManager->getContentFiles(textFiles);
for (const auto &file : attachFiles) {
QString storedPath;
if (ChatSerializer::saveContentToStorage(
m_chatFilePath, file.filename, file.content.toUtf8().toBase64(), storedPath)) {
Context::ContentFile storedFile;
storedFile.filename = file.filename;
storedFile.content = storedPath;
storedAttachments.append(storedFile);
LOG_MESSAGE(QString("Stored text file %1 as %2").arg(file.filename, storedPath));
}
}
} else if (!textFiles.isEmpty()) {
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 text file(s)")
.arg(textFiles.size()));
}
QList<ChatModel::ImageAttachment> imageAttachments;
if (!imageFiles.isEmpty() && !m_chatFilePath.isEmpty()) {
for (const QString &imagePath : imageFiles) {
QString base64Data = encodeImageToBase64(imagePath);
if (base64Data.isEmpty()) {
continue;
}
QString storedPath;
QFileInfo fileInfo(imagePath);
if (ChatSerializer::saveContentToStorage(
m_chatFilePath, fileInfo.fileName(), base64Data, storedPath)) {
ChatModel::ImageAttachment imageAttachment;
imageAttachment.fileName = fileInfo.fileName();
imageAttachment.storedPath = storedPath;
imageAttachment.mediaType = getMediaTypeForImage(imagePath);
imageAttachments.append(imageAttachment);
LOG_MESSAGE(QString("Stored image %1 as %2").arg(fileInfo.fileName(), storedPath));
}
}
} else if (!imageFiles.isEmpty()) {
LOG_MESSAGE(QString("Warning: Chat file path not set, cannot save %1 image(s)")
.arg(imageFiles.size()));
}
m_chatModel->addMessage(message, ChatModel::ChatRole::User, "", storedAttachments, imageAttachments);
auto &chatAssistantSettings = Settings::chatAssistantSettings();
auto providerName = Settings::generalSettings().caProvider();
auto provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (!provider) {
LOG_MESSAGE(QString("No provider found with name: %1").arg(providerName));
return;
}
auto templateName = Settings::generalSettings().caTemplate();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
if (!promptTemplate) {
LOG_MESSAGE(QString("No template found with name: %1").arg(templateName));
return;
}
PluginLLMCore::ContextData context;
const bool isToolsEnabled = useTools;
if (chatAssistantSettings.useSystemPrompt()) {
QString systemPrompt = chatAssistantSettings.systemPrompt();
const QString lastRoleId = chatAssistantSettings.lastUsedRoleId();
if (!lastRoleId.isEmpty()) {
const Settings::AgentRole role = Settings::AgentRolesManager::loadRole(lastRoleId);
if (!role.id.isEmpty())
systemPrompt = systemPrompt + "\n\n" + role.systemPrompt;
}
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
systemPrompt += QString("\n# Active project name: %1").arg(project->displayName());
systemPrompt += QString("\n# Active Project path: %1")
.arg(project->projectDirectory().toUrlishString());
if (auto target = project->activeTarget()) {
if (auto buildConfig = target->activeBuildConfiguration()) {
systemPrompt += QString("\n# Active Build directory: %1")
.arg(buildConfig->buildDirectory().toUrlishString());
}
}
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Chat);
if (!projectRules.isEmpty()) {
systemPrompt += QString("\n# Project Rules\n\n") + projectRules;
}
} else {
systemPrompt += QString("\n# No active project in IDE");
}
if (!linkedFiles.isEmpty()) {
systemPrompt = getSystemPromptWithLinkedFiles(systemPrompt, linkedFiles);
}
context.systemPrompt = systemPrompt;
}
QVector<PluginLLMCore::Message> messages;
for (const auto &msg : m_chatModel->getChatHistory()) {
if (msg.role == ChatModel::ChatRole::Tool || msg.role == ChatModel::ChatRole::FileEdit) {
continue;
}
PluginLLMCore::Message apiMessage;
apiMessage.role = msg.role == ChatModel::ChatRole::User ? "user" : "assistant";
apiMessage.content = msg.content;
if (!msg.attachments.isEmpty() && !m_chatFilePath.isEmpty()) {
apiMessage.content += "\n\nAttached files:";
for (const auto &attachment : msg.attachments) {
QString fileContent = ChatSerializer::loadContentFromStorage(m_chatFilePath, attachment.content);
if (!fileContent.isEmpty()) {
QString decodedContent = QString::fromUtf8(QByteArray::fromBase64(fileContent.toUtf8()));
apiMessage.content += QString("\n\nFile: %1\n```\n%2\n```")
.arg(attachment.filename, decodedContent);
}
}
}
apiMessage.isThinking = (msg.role == ChatModel::ChatRole::Thinking);
apiMessage.isRedacted = msg.isRedacted;
apiMessage.signature = msg.signature;
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)
&& !m_chatFilePath.isEmpty() && !msg.images.isEmpty()) {
auto apiImages = loadImagesFromStorage(msg.images);
if (!apiImages.isEmpty()) {
apiMessage.images = apiImages;
}
}
messages.append(apiMessage);
}
if (!imageFiles.isEmpty()
&& !provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Image)) {
LOG_MESSAGE(QString("Provider %1 doesn't support images, %2 ignored")
.arg(provider->name(), QString::number(imageFiles.size())));
}
context.history = messages;
QJsonObject payload{
{"model", Settings::generalSettings().caModel()}, {"stream", true}};
provider->prepareRequest(
payload,
promptTemplate,
context,
PluginLLMCore::RequestType::Chat,
useTools,
useThinking);
provider->client()->setMaxToolContinuations(
Settings::toolsSettings().maxToolContinuations());
connect(
provider->client(),
&::LLMQore::BaseClient::chunkReceived,
this,
&ClientInterface::handlePartialResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&ClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&ClientInterface::handleRequestFailed,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolStarted,
this,
&ClientInterface::handleToolExecutionStarted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::toolResultReady,
this,
&ClientInterface::handleToolExecutionCompleted,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::thinkingBlockReceived,
this,
&ClientInterface::handleThinkingBlockReceived,
Qt::UniqueConnection);
const QString customEndpoint = Settings::generalSettings().caCustomEndpoint();
const QString endpoint = !customEndpoint.isEmpty() ? customEndpoint
: promptTemplate->endpoint();
auto requestId
= provider->sendRequest(QUrl(Settings::generalSettings().caUrl()), payload, endpoint);
QJsonObject request{{"id", requestId}};
m_activeRequests[requestId] = {request, provider};
emit requestStarted(requestId);
if (provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->setCurrentSessionId(m_chatFilePath);
}
}
}
void ClientInterface::clearMessages()
{
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider && !m_chatFilePath.isEmpty()
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
m_chatModel->clear();
}
void ClientInterface::cancelRequest()
{
QSet<PluginLLMCore::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->client(), nullptr, this, nullptr);
}
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
const RequestContext &ctx = it.value();
if (ctx.provider) {
ctx.provider->cancelRequest(it.key());
}
}
m_activeRequests.clear();
m_accumulatedResponses.clear();
m_awaitingContinuation.clear();
LOG_MESSAGE("All requests cancelled and state cleared");
}
void ClientInterface::handleLLMResponse(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);
}
}
QString ClientInterface::getCurrentFileContext() const
{
auto currentEditor = Core::EditorManager::currentEditor();
if (!currentEditor) {
LOG_MESSAGE("No active editor found");
return QString();
}
auto textDocument = qobject_cast<TextEditor::TextDocument *>(currentEditor->document());
if (!textDocument) {
LOG_MESSAGE("Current document is not a text document");
return QString();
}
QString fileInfo = QString("Language: %1\nFile: %2\n\n")
.arg(textDocument->mimeType(), textDocument->filePath().toFSPathString());
QString content = textDocument->document()->toPlainText();
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 updatedPrompt = basePrompt;
if (!linkedFiles.isEmpty()) {
updatedPrompt += "\n\nLinked files for reference:\n";
auto contentFiles = m_contextManager->getContentFiles(linkedFiles);
for (const auto &file : contentFiles) {
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;
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
m_accumulatedResponses[requestId] += partialText;
const RequestContext &ctx = it.value();
handleLLMResponse(m_accumulatedResponses[requestId], ctx.originalRequest);
}
void ClientInterface::handleFullResponse(const QString &requestId, const QString &fullText)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
const RequestContext &ctx = it.value();
QString finalText = !fullText.isEmpty() ? fullText : m_accumulatedResponses[requestId];
QString applyError;
bool applySuccess
= Context::ChangesManager::instance().applyPendingEditsForRequest(requestId, &applyError);
if (!applySuccess) {
LOG_MESSAGE(QString("Some edits for request %1 were not auto-applied: %2")
.arg(requestId, applyError));
}
LOG_MESSAGE(
"Message completed. Final response for message " + ctx.originalRequest["id"].toString()
+ ": " + finalText);
emit messageReceivedCompletely();
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleRequestFailed(const QString &requestId, const QString &error)
{
auto it = m_activeRequests.find(requestId);
if (it == m_activeRequests.end())
return;
LOG_MESSAGE(QString("Chat request %1 failed: %2").arg(requestId, error));
emit errorOccurred(error);
m_activeRequests.erase(it);
m_accumulatedResponses.remove(requestId);
m_awaitingContinuation.remove(requestId);
}
void ClientInterface::handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring thinking block for non-chat request: %1").arg(requestId));
return;
}
if (m_awaitingContinuation.remove(requestId)) {
m_accumulatedResponses[requestId].clear();
LOG_MESSAGE(
QString("Cleared accumulated responses for continuation request %1").arg(requestId));
}
if (thinking.isEmpty()) {
m_chatModel->addRedactedThinkingBlock(requestId, signature);
} else {
m_chatModel->addThinkingBlock(requestId, thinking, signature);
}
}
void ClientInterface::handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring tool execution start for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->addToolExecutionStatus(requestId, toolId, toolName);
m_awaitingContinuation.insert(requestId);
}
void ClientInterface::handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput)
{
if (!m_activeRequests.contains(requestId)) {
LOG_MESSAGE(QString("Ignoring tool execution result for non-chat request: %1").arg(requestId));
return;
}
m_chatModel->updateToolResult(requestId, toolId, toolName, toolOutput);
}
bool ClientInterface::isImageFile(const QString &filePath) const
{
static const QSet<QString> imageExtensions = {"png", "jpg", "jpeg", "gif", "webp", "bmp", "svg"};
QFileInfo fileInfo(filePath);
QString extension = fileInfo.suffix().toLower();
return imageExtensions.contains(extension);
}
QString ClientInterface::getMediaTypeForImage(const QString &filePath) const
{
static const QHash<QString, QString> mediaTypes
= {{"png", "image/png"},
{"jpg", "image/jpeg"},
{"jpeg", "image/jpeg"},
{"gif", "image/gif"},
{"webp", "image/webp"},
{"bmp", "image/bmp"},
{"svg", "image/svg+xml"}};
QFileInfo fileInfo(filePath);
QString extension = fileInfo.suffix().toLower();
if (mediaTypes.contains(extension)) {
return mediaTypes[extension];
}
QMimeDatabase mimeDb;
QMimeType mimeType = mimeDb.mimeTypeForFile(filePath);
return mimeType.name();
}
QString ClientInterface::encodeImageToBase64(const QString &filePath) const
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
LOG_MESSAGE(QString("Failed to open image file: %1").arg(filePath));
return QString();
}
QByteArray imageData = file.readAll();
file.close();
return imageData.toBase64();
}
QVector<PluginLLMCore::ImageAttachment> ClientInterface::loadImagesFromStorage(
const QList<ChatModel::ImageAttachment> &storedImages) const
{
QVector<PluginLLMCore::ImageAttachment> apiImages;
for (const auto &storedImage : storedImages) {
QString base64Data
= ChatSerializer::loadContentFromStorage(m_chatFilePath, storedImage.storedPath);
if (base64Data.isEmpty()) {
LOG_MESSAGE(QString("Warning: Failed to load image: %1").arg(storedImage.storedPath));
continue;
}
PluginLLMCore::ImageAttachment apiImage;
apiImage.data = base64Data;
apiImage.mediaType = storedImage.mediaType;
apiImage.isUrl = false;
apiImages.append(apiImage);
}
return apiImages;
}
void ClientInterface::setChatFilePath(const QString &filePath)
{
if (!m_chatFilePath.isEmpty() && m_chatFilePath != filePath) {
const auto providerName = Settings::generalSettings().caProvider();
auto *provider = PluginLLMCore::ProvidersManager::instance().getProviderByName(providerName);
if (provider
&& provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::Tools)
&& provider->toolsManager()) {
if (auto *todoTool = qobject_cast<QodeAssist::Tools::TodoTool *>(
provider->toolsManager()->tool("todo_tool"))) {
todoTool->clearSession(m_chatFilePath);
}
}
}
m_chatFilePath = filePath;
m_chatModel->setChatFilePath(filePath);
}
QString ClientInterface::chatFilePath() const
{
return m_chatFilePath;
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,86 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QSet>
#include <QString>
#include <QVector>
#include "ChatModel.hpp"
#include "Provider.hpp"
#include "pluginllmcore/IPromptProvider.hpp"
#include <context/ContextManager.hpp>
namespace QodeAssist::Chat {
class ClientInterface : public QObject
{
Q_OBJECT
public:
explicit ClientInterface(
ChatModel *chatModel, PluginLLMCore::IPromptProvider *promptProvider, QObject *parent = nullptr);
~ClientInterface();
void sendMessage(
const QString &message,
const QList<QString> &attachments = {},
const QList<QString> &linkedFiles = {},
bool useTools = false,
bool useThinking = 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 handleThinkingBlockReceived(
const QString &requestId, const QString &thinking, const QString &signature);
void handleToolExecutionStarted(
const QString &requestId, const QString &toolId, const QString &toolName);
void handleToolExecutionCompleted(
const QString &requestId,
const QString &toolId,
const QString &toolName,
const QString &toolOutput);
private:
void handleLLMResponse(const QString &response, const QJsonObject &request);
QString getCurrentFileContext() const;
QString getSystemPromptWithLinkedFiles(
const QString &basePrompt, const QList<QString> &linkedFiles) const;
bool isImageFile(const QString &filePath) const;
QString getMediaTypeForImage(const QString &filePath) const;
QString encodeImageToBase64(const QString &filePath) const;
QVector<PluginLLMCore::ImageAttachment> loadImagesFromStorage(const QList<ChatModel::ImageAttachment> &storedImages) const;
struct RequestContext
{
QJsonObject originalRequest;
PluginLLMCore::Provider *provider;
};
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
ChatModel *m_chatModel;
Context::ContextManager *m_contextManager;
QString m_chatFilePath;
QHash<QString, RequestContext> m_activeRequests;
QHash<QString, QString> m_accumulatedResponses;
QSet<QString> m_awaitingContinuation;
};
} // namespace QodeAssist::Chat

60
ChatView/FileItem.cpp Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FileItem.hpp"
#include <QDesktopServices>
#include <QUrl>
#include <coreplugin/editormanager/editormanager.h>
#include <logger/Logger.hpp>
#include <utils/filepath.h>
namespace QodeAssist::Chat {
FileItem::FileItem(QQuickItem *parent)
: QQuickItem(parent)
{}
void FileItem::openFileInEditor()
{
if (m_filePath.isEmpty()) {
return;
}
Utils::FilePath filePathObj = Utils::FilePath::fromString(m_filePath);
Core::IEditor *editor = Core::EditorManager::openEditor(filePathObj);
if (!editor) {
LOG_MESSAGE(QString("Failed to open file in editor: %1").arg(m_filePath));
}
}
void FileItem::openFileInExternalEditor()
{
if (m_filePath.isEmpty()) {
return;
}
bool success = QDesktopServices::openUrl(QUrl::fromLocalFile(m_filePath));
if (success) {
LOG_MESSAGE(QString("Opened file in external application: %1").arg(m_filePath));
} else {
LOG_MESSAGE(QString("Failed to open file externally: %1").arg(m_filePath));
}
}
QString FileItem::filePath() const
{
return m_filePath;
}
void FileItem::setFilePath(const QString &newFilePath)
{
if (m_filePath == newFilePath)
return;
m_filePath = newFilePath;
emit filePathChanged();
}
} // namespace QodeAssist::Chat

32
ChatView/FileItem.hpp Normal file
View File

@@ -0,0 +1,32 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QQuickItem>
namespace QodeAssist::Chat {
class FileItem: public QQuickItem
{
Q_OBJECT
QML_NAMED_ELEMENT(FileItem)
Q_PROPERTY(QString filePath READ filePath WRITE setFilePath NOTIFY filePathChanged)
public:
FileItem(QQuickItem *parent = nullptr);
Q_INVOKABLE void openFileInEditor();
Q_INVOKABLE void openFileInExternalEditor();
QString filePath() const;
void setFilePath(const QString &newFilePath);
signals:
void filePathChanged();
private:
QString m_filePath;
};
}

View File

@@ -0,0 +1,426 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "FileMentionItem.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QTextStream>
#include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h>
#include <projectexplorer/project.h>
#include <projectexplorer/projectmanager.h>
namespace QodeAssist::Chat {
FileMentionItem::FileMentionItem(QQuickItem *parent)
: QQuickItem(parent)
{}
QVariantList FileMentionItem::searchResults() const
{
return m_searchResults;
}
int FileMentionItem::currentIndex() const
{
return m_currentIndex;
}
void FileMentionItem::setCurrentIndex(int index)
{
if (m_currentIndex == index)
return;
m_currentIndex = index;
emit currentIndexChanged();
}
void FileMentionItem::updateSearch(const QString &query)
{
m_lastQuery = query;
QVariantList openFiles = getOpenFiles(query);
QVariantList projectResults = searchProjectFiles(query);
QSet<QString> openPaths;
for (const QVariant &item : std::as_const(openFiles)) {
const QVariantMap map = item.toMap();
openPaths.insert(map.value("absolutePath").toString());
}
QVariantList combined = openFiles;
for (const QVariant &item : std::as_const(projectResults)) {
const QVariantMap map = item.toMap();
if (!map.value("isProject").toBool()
&& openPaths.contains(map.value("absolutePath").toString()))
continue;
combined.append(item);
}
m_searchResults = combined;
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
}
void FileMentionItem::refreshSearch()
{
if (!m_lastQuery.isNull())
updateSearch(m_lastQuery);
}
void FileMentionItem::moveUp()
{
if (m_currentIndex > 0) {
m_currentIndex--;
emit currentIndexChanged();
}
}
void FileMentionItem::moveDown()
{
if (m_currentIndex < m_searchResults.size() - 1) {
m_currentIndex++;
emit currentIndexChanged();
}
}
void FileMentionItem::selectCurrent()
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size())
return;
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
if (item.value("isProject").toBool()) {
emit projectSelected(item.value("projectName").toString());
} else {
emit fileSelected(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString());
}
}
void FileMentionItem::dismiss()
{
m_searchResults.clear();
m_currentIndex = 0;
emit searchResultsChanged();
emit currentIndexChanged();
emit dismissed();
}
QVariantMap FileMentionItem::applyCurrentSelection(
const QString &text, int cursorPosition, bool useTools)
{
if (m_currentIndex < 0 || m_currentIndex >= m_searchResults.size()) {
dismiss();
return {};
}
const QString textBefore = text.left(cursorPosition);
const int atIndex = textBefore.lastIndexOf('@');
if (atIndex < 0) {
dismiss();
return {};
}
const QVariantMap item = m_searchResults[m_currentIndex].toMap();
QString replacement;
if (item.value("isProject").toBool()) {
replacement = QStringLiteral("@") + item.value("projectName").toString() + ":";
} else {
const QString currentQuery = textBefore.mid(atIndex + 1);
const QVariantMap result = handleFileSelection(
item.value("absolutePath").toString(),
item.value("relativePath").toString(),
item.value("projectName").toString(),
currentQuery,
useTools);
if (result.value("mode").toString() == "mention")
replacement = result.value("mentionText").toString();
}
const QString newText = text.left(atIndex) + replacement + text.mid(cursorPosition);
const int newCursorPosition = atIndex + replacement.length();
dismiss();
return {{"text", newText}, {"cursorPosition", newCursorPosition}};
}
QVariantMap FileMentionItem::handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools)
{
QVariantMap result;
const QString fileName = relativePath.section('/', -1);
QString mentionKey = fileName;
const int colonIdx = currentQuery.indexOf(':');
if (colonIdx > 0) {
const QString projPrefix = currentQuery.left(colonIdx);
if (projPrefix.compare(projectName, Qt::CaseInsensitive) == 0)
mentionKey = projPrefix + ":" + fileName;
}
if (useTools) {
registerMention(mentionKey, absolutePath);
result["mode"] = QStringLiteral("mention");
result["mentionText"] = "@" + mentionKey + " ";
} else {
emit fileAttachRequested({absolutePath});
result["mode"] = QStringLiteral("attach");
}
return result;
}
void FileMentionItem::registerMention(const QString &mentionKey, const QString &absolutePath)
{
m_atMentionMap[mentionKey] = absolutePath;
}
void FileMentionItem::clearMentions()
{
m_atMentionMap.clear();
}
QString FileMentionItem::expandMentions(const QString &text)
{
QString result = text;
for (auto it = m_atMentionMap.constBegin(); it != m_atMentionMap.constEnd(); ++it) {
const QString &mentionKey = it.key();
const QString &absPath = it.value();
const QString displayName = mentionKey.section(':', -1);
const QString escaped = QRegularExpression::escape(mentionKey);
// @key:N-M -> hyperlink + inline code block
const QRegularExpression rangeRe("@" + escaped + ":(\\d+)-(\\d+)(?=\\s|$)");
QRegularExpressionMatchIterator matchIt = rangeRe.globalMatch(result);
QList<QRegularExpressionMatch> matches;
while (matchIt.hasNext())
matches.append(matchIt.next());
for (int i = matches.size() - 1; i >= 0; --i) {
const auto &m = matches[i];
const int startLine = m.captured(1).toInt();
const int endLine = m.captured(2).toInt();
const QString ext = fileExtension(absPath);
const QString snippet = readFileLines(absPath, startLine, endLine);
const QString replacement
= QString("[@%1:%2-%3](file://%4)\n```%5\n%6```")
.arg(displayName)
.arg(startLine)
.arg(endLine)
.arg(absPath, ext, snippet);
result.replace(m.capturedStart(), m.capturedLength(), replacement);
}
// @key -> hyperlink only
const QRegularExpression simpleRe("@" + escaped + "(?=\\s|$)");
result.replace(simpleRe, QString("[@%1](file://%2)").arg(displayName, absPath));
}
return result;
}
QVariantList FileMentionItem::searchProjectFiles(const QString &query)
{
QVariantList results;
struct FileResult
{
QString absolutePath;
QString relativePath;
QString projectName;
int priority;
};
const auto allProjects = ProjectExplorer::ProjectManager::projects();
QString projectFilter;
QString fileQuery = query;
const int colonIdx = query.indexOf(':');
if (colonIdx > 0) {
const QString prefix = query.left(colonIdx);
for (auto project : allProjects) {
if (project && project->displayName().compare(prefix, Qt::CaseInsensitive) == 0) {
projectFilter = project->displayName();
fileQuery = query.mid(colonIdx + 1);
break;
}
}
}
if (projectFilter.isEmpty() && colonIdx < 0) {
const QString lowerQ = query.toLower();
for (auto project : allProjects) {
if (!project)
continue;
const QString name = project->displayName();
if (query.isEmpty() || name.toLower().startsWith(lowerQ)) {
QVariantMap item;
item["absolutePath"] = QString();
item["relativePath"] = name;
item["projectName"] = name;
item["isProject"] = true;
results.append(item);
}
}
}
QList<FileResult> candidates;
const QString lowerFileQuery = fileQuery.toLower();
const bool emptyFileQuery = fileQuery.isEmpty();
for (auto project : allProjects) {
if (!project)
continue;
if (!projectFilter.isEmpty() && project->displayName() != projectFilter)
continue;
const auto projectFiles = project->files(ProjectExplorer::Project::SourceFiles);
const QString projectDir = project->projectDirectory().path();
const QString projectName = project->displayName();
for (const auto &filePath : projectFiles) {
const QString absolutePath = filePath.path();
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
const QString relativePath = QDir(projectDir).relativeFilePath(absolutePath);
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
int priority = -1;
if (emptyFileQuery) {
priority = 3;
} else if (lowerFileName == lowerFileQuery) {
priority = 0;
} else if (lowerFileName.startsWith(lowerFileQuery)) {
priority = 1;
} else if (lowerFileName.contains(lowerFileQuery)) {
priority = 2;
} else if (lowerRelativePath.contains(lowerFileQuery)) {
priority = 3;
}
if (priority >= 0)
candidates.append({absolutePath, relativePath, projectName, priority});
}
}
std::sort(candidates.begin(), candidates.end(), [](const FileResult &a, const FileResult &b) {
if (a.priority != b.priority)
return a.priority < b.priority;
return a.relativePath < b.relativePath;
});
const int maxFiles = qMax(0, 10 - results.size());
const int count = qMin(candidates.size(), maxFiles);
for (int i = 0; i < count; i++) {
QVariantMap item;
item["absolutePath"] = candidates[i].absolutePath;
item["relativePath"] = candidates[i].relativePath;
item["projectName"] = candidates[i].projectName;
item["isProject"] = false;
results.append(item);
}
return results;
}
QVariantList FileMentionItem::getOpenFiles(const QString &query)
{
QVariantList results;
const QString lowerQuery = query.toLower();
const bool emptyQuery = query.isEmpty();
QSet<QString> addedPaths;
auto tryAddDocument = [&](Core::IDocument *document) {
if (!document)
return;
const QString absolutePath = document->filePath().toFSPathString();
if (absolutePath.isEmpty() || addedPaths.contains(absolutePath))
return;
const QFileInfo fileInfo(absolutePath);
const QString fileName = fileInfo.fileName();
if (fileName.isEmpty())
return;
QString relativePath = absolutePath;
QString projectName;
auto project = ProjectExplorer::ProjectManager::projectForFile(document->filePath());
if (project) {
projectName = project->displayName();
relativePath = QDir(project->projectDirectory().path()).relativeFilePath(absolutePath);
}
if (!emptyQuery) {
const QString lowerFileName = fileName.toLower();
const QString lowerRelativePath = relativePath.toLower();
if (!lowerFileName.contains(lowerQuery) && !lowerRelativePath.contains(lowerQuery))
return;
}
addedPaths.insert(absolutePath);
QVariantMap item;
item["absolutePath"] = absolutePath;
item["relativePath"] = relativePath;
item["projectName"] = projectName;
item["isProject"] = false;
item["isOpen"] = true;
results.append(item);
};
if (auto current = Core::EditorManager::currentEditor())
tryAddDocument(current->document());
for (auto editor : Core::EditorManager::visibleEditors())
if (editor)
tryAddDocument(editor->document());
for (auto document : Core::DocumentModel::openedDocuments())
tryAddDocument(document);
return results;
}
QString FileMentionItem::readFileLines(const QString &filePath, int startLine, int endLine)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return {};
QTextStream stream(&file);
QString result;
int lineNum = 1;
while (!stream.atEnd()) {
const QString line = stream.readLine();
if (lineNum >= startLine)
result += line + '\n';
if (lineNum >= endLine)
break;
++lineNum;
}
return result;
}
QString FileMentionItem::fileExtension(const QString &filePath)
{
const int dot = filePath.lastIndexOf('.');
return dot >= 0 ? filePath.mid(dot + 1) : QString();
}
} // namespace QodeAssist::Chat

View File

@@ -0,0 +1,70 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QHash>
#include <QQuickItem>
#include <QRegularExpression>
#include <QVariantList>
namespace QodeAssist::Chat {
class FileMentionItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QVariantList searchResults READ searchResults NOTIFY searchResultsChanged FINAL)
Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged FINAL)
QML_ELEMENT
public:
explicit FileMentionItem(QQuickItem *parent = nullptr);
QVariantList searchResults() const;
int currentIndex() const;
void setCurrentIndex(int index);
Q_INVOKABLE void updateSearch(const QString &query);
Q_INVOKABLE void refreshSearch();
Q_INVOKABLE void moveUp();
Q_INVOKABLE void moveDown();
Q_INVOKABLE void selectCurrent();
Q_INVOKABLE void dismiss();
Q_INVOKABLE QVariantMap handleFileSelection(
const QString &absolutePath,
const QString &relativePath,
const QString &projectName,
const QString &currentQuery,
bool useTools);
Q_INVOKABLE QVariantMap applyCurrentSelection(
const QString &text, int cursorPosition, bool useTools);
Q_INVOKABLE void registerMention(const QString &mentionKey, const QString &absolutePath);
Q_INVOKABLE void clearMentions();
Q_INVOKABLE QString expandMentions(const QString &text);
signals:
void searchResultsChanged();
void currentIndexChanged();
void fileSelected(const QString &absolutePath,
const QString &relativePath,
const QString &projectName);
void projectSelected(const QString &projectName);
void dismissed();
void fileAttachRequested(const QStringList &filePaths);
private:
QVariantList searchProjectFiles(const QString &query);
QVariantList getOpenFiles(const QString &query);
QString readFileLines(const QString &filePath, int startLine, int endLine);
static QString fileExtension(const QString &filePath);
QVariantList m_searchResults;
int m_currentIndex = 0;
QString m_lastQuery;
QHash<QString, QString> m_atMentionMap;
};
} // namespace QodeAssist::Chat

30
ChatView/MessagePart.hpp Normal file
View File

@@ -0,0 +1,30 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QtQmlIntegration>
#include "ChatData.hpp"
namespace QodeAssist::Chat {
class MessagePart
{
Q_GADGET
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:
MessagePartType type;
QString text;
QString language;
QString imageData; // Base64 data or URL
QString mediaType; // e.g., "image/png", "image/jpeg"
};
} // 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,11 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_37_14)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_37_14">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 869 B

View File

@@ -0,0 +1,11 @@
<svg width="24" height="48" viewBox="0 0 24 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_20)">
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
<path d="M22 10.1053V36.7368C22 41.8547 17.525 46 12 46C6.475 46 2 41.8547 2 36.7368V7.78947C2 4.59368 4.8 2 8.25 2C11.7 2 15.75 4.59368 15.75 7.78947V35.5789C15.75 36.8526 13.375 39.0526 12 39.0526C10.625 39.0526 8.25 36.8526 8.25 35.5789V21.6842V8.94737" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_20">
<rect width="24" height="48" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 869 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,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_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 353 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_41_14)">
<path d="M0 0L24 24M0 24L24 0" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_41_14">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<!-- Archive/compress icon: downward arrows pointing to center with horizontal lines -->
<line x1="12" y1="3" x2="12" y2="10" />
<polyline points="9 7 12 10 15 7" />
<line x1="12" y1="21" x2="12" y2="14" />
<polyline points="9 17 12 14 15 17" />
<line x1="4" y1="12" x2="20" y2="12" stroke-width="3" />
</svg>

After

Width:  |  Height:  |  Size: 487 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M3 5h18v2H3V5zm0 6h18v2H3v-2zm0 6h12v2H3v-2z"/>
<circle cx="19" cy="17" r="3" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

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,12 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_49_24)">
<path d="M10 12L10 32L10 12Z" fill="black"/>
<path d="M10 12L10 32" stroke="black" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="black" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_49_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View File

@@ -0,0 +1,12 @@
<svg width="20" height="44" viewBox="0 0 20 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_51_24)">
<path d="M10 12L10 32Z" fill="white"/>
<path d="M10 12L10 32" stroke="white" stroke-width="3"/>
<path d="M1.50001 12.484C1.50001 -1.99999 18.5 -1.99999 18.5 12.484M1.5 31.5334C1.50001 46 18.5 46 18.5 31.5334" stroke="white" stroke-width="3" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_51_24">
<rect width="20" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 507 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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 962 B

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

After

Width:  |  Height:  |  Size: 1.8 KiB

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,11 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_82_71)">
<path d="M10.7777 0.0435181C14.2316 -0.253961 17.6161 0.979215 20.0629 3.42633C23.4139 6.77767 24.3012 11.6719 22.7299 15.8433C22.9016 15.988 23.0706 16.1419 23.2377 16.3072L42.2221 34.2203C42.2288 34.2268 42.2353 34.2344 42.2426 34.2408C44.4752 36.4735 44.4752 40.1064 42.2426 42.3394C40.0096 44.5717 36.4035 44.5446 34.1713 42.3121C34.1617 42.3031 34.1528 42.2937 34.144 42.2838L16.3871 23.1519C16.2254 22.9894 16.0746 22.8196 15.933 22.6451C11.7604 24.2194 6.86327 23.3335 3.50919 19.98C1.06298 17.5327 -0.171482 14.1483 0.126373 10.6949C0.160109 10.3034 0.41818 9.96685 0.78653 9.83258C1.15602 9.69759 1.57009 9.78945 1.84805 10.067L7.53555 15.7535L13.8402 13.7574L15.8363 7.4527L10.1488 1.7652C9.87057 1.48716 9.77945 1.07345 9.91348 0.704651C10.0489 0.335072 10.3852 0.0774496 10.7777 0.0435181ZM37.3656 34.7496L37.3129 34.9302L37.1586 35.4673L36.8363 35.5679L36.6195 35.2047L36.4623 34.942L36.2357 35.148L35.725 35.6148L35.5746 35.7525L35.6791 35.9283L35.9184 36.3287L35.7104 36.6548L35.1742 36.5543L34.9408 36.5093L34.8852 36.7418L34.7572 37.275L34.7123 37.4644L34.8842 37.5543L35.3891 37.8179V38.1802L34.8842 38.4449L34.7123 38.5347L34.7572 38.7242L34.8852 39.2574L34.9408 39.4898L35.1742 39.4449L35.7104 39.3433L35.9184 39.6695L35.6791 40.0709L35.5746 40.2466L35.725 40.3843L36.2357 40.8511L36.4623 41.0572L36.6195 40.7945L36.8363 40.4302L37.1586 40.5308L37.3129 41.0689L37.3656 41.2496H38.6352L38.6879 41.0689L38.8412 40.5308L39.1635 40.4302L39.3813 40.7945L39.5385 41.0572L39.765 40.8511L40.2758 40.3843L40.4262 40.2466L40.3217 40.0709L40.0815 39.6695L40.2895 39.3433L40.8266 39.4449L41.06 39.4898L41.1156 39.2574L41.2436 38.7242L41.2885 38.5347L41.1166 38.4449L40.6117 38.1802V37.8179L41.1166 37.5543L41.2885 37.4644L41.2436 37.275L41.1156 36.7418L41.06 36.5093L40.8266 36.5543L40.2895 36.6548L40.0815 36.3287L40.3217 35.9283L40.4262 35.7525L40.2758 35.6148L39.765 35.148L39.5385 34.942L39.3813 35.2047L39.1635 35.5679L38.8412 35.4673L38.6879 34.9302L38.6352 34.7496H37.3656Z" fill="black" fill-opacity="0.6"/>
<path d="M6 36L38 7" stroke="black" stroke-opacity="0.6" stroke-width="4" stroke-linecap="round"/>
</g>
<defs>
<clipPath id="clip0_82_71">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,10 @@
<svg width="44" height="44" viewBox="0 0 44 44" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_82_50)">
<path d="M10.7775 0.0441895C14.2315 -0.253375 17.6159 0.979824 20.0627 3.427C23.4135 6.77842 24.3011 11.6726 22.7297 15.844C22.9013 15.9886 23.0714 16.1416 23.2385 16.3069L42.2219 34.2209C42.2285 34.2274 42.2352 34.2341 42.2424 34.2405C44.475 36.4732 44.475 40.1061 42.2424 42.3391C40.0094 44.5715 36.4033 44.5444 34.1711 42.3118C34.1615 42.3028 34.1525 42.2934 34.1437 42.2834L16.3869 23.1516C16.2251 22.9891 16.0745 22.8193 15.9328 22.6448C11.7602 24.2191 6.86304 23.3333 3.50897 19.9797C1.06276 17.5324 -0.171773 14.148 0.12616 10.6946C0.159908 10.3029 0.418723 9.96644 0.787292 9.83228C1.15667 9.69748 1.56997 9.78926 1.84784 10.0667L7.53534 15.7532L13.84 13.7571L15.8361 7.45239L10.1486 1.76489C9.87052 1.48684 9.78022 1.07306 9.91425 0.704346C10.0498 0.334991 10.3852 0.0781082 10.7775 0.0441895ZM37.3654 34.7502L37.3127 34.9309L37.1584 35.468L36.8361 35.5686L36.6193 35.2053L36.4621 34.9426L36.2355 35.1487L35.7248 35.6155L35.5744 35.7532L35.6789 35.929L35.9182 36.3293L35.7101 36.6555L35.174 36.5549L34.9406 36.51L34.8849 36.7424L34.757 37.2756L34.7121 37.4651L34.884 37.5549L35.3889 37.8186V38.1809L34.884 38.4456L34.7121 38.5354L34.757 38.7249L34.8849 39.2581L34.9406 39.4905L35.174 39.4456L35.7101 39.344L35.9182 39.6702L35.6789 40.0715L35.5744 40.2473L35.7248 40.385L36.2355 40.8518L36.4621 41.0579L36.6193 40.7952L36.8361 40.4309L37.1584 40.5315L37.3127 41.0696L37.3654 41.2502H38.6349L38.6877 41.0696L38.841 40.5315L39.1633 40.4309L39.381 40.7952L39.5383 41.0579L39.7648 40.8518L40.2756 40.385L40.426 40.2473L40.3215 40.0715L40.0812 39.6702L40.2892 39.344L40.8264 39.4456L41.0598 39.4905L41.1154 39.2581L41.2433 38.7249L41.2883 38.5354L41.1164 38.4456L40.6115 38.1809V37.8186L41.1164 37.5549L41.2883 37.4651L41.2433 37.2756L41.1154 36.7424L41.0598 36.51L40.8264 36.5549L40.2892 36.6555L40.0812 36.3293L40.3215 35.929L40.426 35.7532L40.2756 35.6155L39.7648 35.1487L39.5383 34.9426L39.381 35.2053L39.1633 35.5686L38.841 35.468L38.6877 34.9309L38.6349 34.7502H37.3654Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_82_50">
<rect width="44" height="44" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

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

642
ChatView/qml/RootItem.qml Normal file
View File

@@ -0,0 +1,642 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic as QQC
import QtQuick.Layouts
import ChatView
import UIControls
import Qt.labs.platform as Platform
import "./chatparts"
import "./controls"
ChatRootView {
id: root
property SystemPalette sysPalette: SystemPalette {
colorGroup: SystemPalette.Active
}
palette {
window: sysPalette.window
windowText: sysPalette.windowText
base: sysPalette.base
alternateBase: sysPalette.alternateBase
text: sysPalette.text
button: sysPalette.button
buttonText: sysPalette.buttonText
highlight: sysPalette.highlight
highlightedText: sysPalette.highlightedText
light: sysPalette.light
mid: sysPalette.mid
dark: sysPalette.dark
shadow: sysPalette.shadow
brightText: sysPalette.brightText
}
Rectangle {
id: bg
anchors.fill: parent
color: palette.window
}
SplitDropZone {
anchors.fill: parent
z: 99
onFilesDroppedToAttach: (urlStrings) => {
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
if (localPaths.length > 0) {
root.addFilesToAttachList(localPaths)
}
}
onFilesDroppedToLink: (urlStrings) => {
var localPaths = root.convertUrlsToLocalPaths(urlStrings)
if (localPaths.length > 0) {
root.addFilesToLinkList(localPaths)
}
}
}
QoABusyOverlay {
id: compressingOverlay
z: 50
anchors.fill: mainColumn
anchors.topMargin: topBar.height
anchors.bottomMargin: bottomBar.height
active: root.isCompressing
text: qsTr("Compressing chat…")
}
ColumnLayout {
id: mainColumn
anchors.fill: parent
spacing: 0
TopBar {
id: topBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + 10
saveButton.onClicked: root.showSaveDialog()
loadButton.onClicked: root.showLoadDialog()
clearButton.onClicked: root.clearChat()
tokensBadge {
text: qsTr("%1/%2").arg(root.inputTokensCount).arg(root.chatModel.tokensThreshold)
}
recentPath {
text: qsTr("Сhat name: %1").arg(root.chatFileName.length > 0 ? root.chatFileName : "Unsaved")
}
openChatHistory.onClicked: root.openChatHistoryFolder()
contextButton.onClicked: contextViewer.open()
pinButton {
visible: typeof _chatview !== 'undefined'
checked: typeof _chatview !== 'undefined' ? _chatview.isPin : false
onCheckedChanged: _chatview.isPin = topBar.pinButton.checked
}
toolsButton {
checked: root.useTools
onCheckedChanged: {
root.useTools = toolsButton.checked
}
}
thinkingMode {
checked: root.useThinking
enabled: root.isThinkingSupport
onCheckedChanged: {
root.useThinking = thinkingMode.checked
}
}
settingsButton.onClicked: root.openSettings()
configSelector {
model: root.availableConfigurations
displayText: root.currentConfiguration
onActivated: function(index) {
if (index > 0) {
root.applyConfiguration(root.availableConfigurations[index])
}
}
popup.onAboutToShow: {
root.loadAvailableConfigurations()
}
}
roleSelector {
model: root.availableAgentRoles
displayText: root.currentAgentRole
onActivated: function(index) {
root.applyAgentRole(root.availableAgentRoles[index])
}
popup.onAboutToShow: {
root.loadAvailableAgentRoles()
}
}
}
ListView {
id: chatListView
property bool userScrolledUp: false
Layout.fillWidth: true
Layout.fillHeight: true
leftMargin: 5
model: root.chatModel
clip: true
spacing: 0
boundsBehavior: Flickable.StopAtBounds
cacheBuffer: 2000
onMovingChanged: {
if (moving) {
userScrolledUp = !atYEnd
}
}
onAtYEndChanged: {
if (atYEnd) {
userScrolledUp = false
}
}
delegate: Loader {
id: componentLoader
required property var model
required property int index
width: ListView.view.width - scroll.width
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
}
}
}
header: Item {
width: ListView.view.width - scroll.width
height: 30
}
ScrollBar.vertical: QQC.ScrollBar {
id: scroll
}
Rectangle {
id: scrollToBottomButton
anchors {
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
bottomMargin: 10
}
width: 36
height: 36
radius: 18
color: palette.button
border.color: palette.mid
border.width: 1
visible: chatListView.userScrolledUp
opacity: 0.9
z: 100
Text {
anchors.centerIn: parent
text: "▼"
font.pixelSize: 14
color: palette.buttonText
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
chatListView.userScrolledUp = false
root.scrollToBottom()
}
}
Behavior on visible {
enabled: false
}
}
onCountChanged: {
if (!userScrolledUp) {
root.scrollToBottom()
}
}
onContentHeightChanged: {
if (!userScrolledUp && atYEnd) {
root.scrollToBottom()
}
}
Component {
id: chatItemComponent
ChatItem {
id: chatItemInstance
width: parent.width
msgModel: root.chatModel.processMessageContent(model.content)
messageAttachments: model.attachments
messageImages: model.images
chatFilePath: root.chatFilePath()
isUserMessage: model.roleType === ChatModel.User
messageIndex: index
textFontFamily: root.textFontFamily
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
textFontSize: root.textFontSize
textFormat: root.textFormat
onResetChatToMessage: function(idx) {
messageInput.text = model.content
messageInput.cursorPosition = model.content.length
root.chatModel.resetModelTo(idx)
}
onOpenFileRequested: function(filePath) {
root.openFileInEditor(filePath)
}
}
}
Component {
id: toolMessageComponent
ToolBlock {
width: parent.width
toolContent: model.content
}
}
Component {
id: fileEditMessageComponent
FileEditBlock {
width: parent.width
editContent: model.content
onApplyEdit: function(editId) {
root.applyFileEdit(editId)
}
onRejectEdit: function(editId) {
root.rejectFileEdit(editId)
}
onUndoEdit: function(editId) {
root.undoFileEdit(editId)
}
onOpenInEditor: function(editId) {
root.openFileEditInEditor(editId)
}
}
}
Component {
id: thinkingMessageComponent
ThinkingBlock {
width: parent.width
thinkingContent: {
let content = model.content
let signatureStart = content.indexOf("\n[Signature:")
if (signatureStart >= 0) {
return content.substring(0, signatureStart)
}
return content
}
isRedacted: model.isRedacted !== undefined ? model.isRedacted : false
}
}
}
ScrollView {
id: view
Layout.fillWidth: true
Layout.minimumHeight: 30
Layout.maximumHeight: root.height / 2
QQC.TextArea {
id: messageInput
placeholderText: Qt.platform.os === "osx"
? qsTr("Type your message here... (⌘+↩ to send)")
: qsTr("Type your message here... (Ctrl+Enter to send)")
placeholderTextColor: palette.mid
color: palette.text
background: Rectangle {
radius: 2
color: palette.base
border.color: messageInput.activeFocus ? palette.highlight : palette.button
border.width: 1
Behavior on border.color {
ColorAnimation { duration: 150 }
}
Rectangle {
anchors.fill: parent
color: palette.highlight
opacity: messageInput.hovered ? 0.1 : 0
radius: parent.radius
}
}
onTextChanged: {
root.calculateMessageTokensCount(messageInput.text)
var cursorPos = messageInput.cursorPosition
var textBefore = messageInput.text.substring(0, cursorPos)
var atIndex = textBefore.lastIndexOf('@')
if (atIndex >= 0) {
var query = textBefore.substring(atIndex + 1)
if (query.indexOf(' ') === -1 && query.indexOf('\n') === -1) {
fileMentionPopup.updateSearch(query)
return
}
}
fileMentionPopup.dismiss()
}
Keys.onPressed: function(event) {
if (fileMentionPopup.visible) {
if (event.key === Qt.Key_Down) {
fileMentionPopup.moveDown()
event.accepted = true
} else if (event.key === Qt.Key_Up) {
fileMentionPopup.moveUp()
event.accepted = true
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
root.applyMentionSelection()
event.accepted = true
} else if (event.key === Qt.Key_Escape) {
fileMentionPopup.dismiss()
event.accepted = true
}
}
}
MouseArea {
anchors.fill: parent
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
Layout.fillWidth: true
attachedFilesModel: root.attachmentFiles
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/attach-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.8, 0.3, 0.4))
onRemoveFileFromListByIndex: (index) => root.removeFileFromAttachList(index)
}
AttachedFilesPlace {
id: linkedFilesPlace
Layout.fillWidth: true
attachedFilesModel: root.linkedFiles
iconPath: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
: "qrc:/qt/qml/ChatView/icons/link-file-light.svg"
accentColor: Qt.tint(palette.mid, Qt.rgba(0, 0.3, 0.8, 0.4))
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
isCompressing: root.isCompressing
sendButton.onClicked: !root.isRequestInProgress ? root.sendChatMessage()
: root.cancelRequest()
sendButton.icon.source: !root.isRequestInProgress ? "qrc:/qt/qml/ChatView/icons/chat-icon.svg"
: "qrc:/qt/qml/ChatView/icons/chat-pause-icon.svg"
sendButton.text: !root.isRequestInProgress ? qsTr("Send") : qsTr("Stop")
sendButton.ToolTip.text: !root.isRequestInProgress ? qsTr("Send message to LLM %1").arg(Qt.platform.os === "osx" ? "Cmd+Return" : "Ctrl+Return")
: qsTr("Stop")
compressButton.onClicked: compressConfirmDialog.open()
cancelCompressButton.onClicked: root.cancelCompression()
syncOpenFiles {
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
enabled: messageInput.activeFocus && !Qt.inputMethod.visible && !fileMentionPopup.visible
onActivated: root.sendChatMessage()
}
function clearChat() {
root.clearMessages()
root.clearAttachmentFiles()
root.updateInputTokensCount()
}
function scrollToBottom() {
Qt.callLater(chatListView.positionViewAtEnd)
}
function applyMentionSelection() {
var result = fileMentionPopup.applyCurrentSelection(
messageInput.text, messageInput.cursorPosition, root.useTools)
if (result.text !== undefined) {
messageInput.text = result.text
messageInput.cursorPosition = result.cursorPosition
}
}
function sendChatMessage() {
root.sendMessage(fileMentionPopup.expandMentions(messageInput.text))
messageInput.text = ""
fileMentionPopup.clearMentions()
scrollToBottom()
}
Dialog {
id: compressConfirmDialog
anchors.centerIn: parent
title: qsTr("Compress Chat")
modal: true
standardButtons: Dialog.Yes | Dialog.No
Label {
text: qsTr("Create a summarized copy of this chat?\n\nThe summary will be generated by LLM and saved as a new chat file.")
wrapMode: Text.WordWrap
}
onAccepted: root.compressCurrentChat()
}
Toast {
id: errorToast
z: 1000
color: Qt.rgba(0.8, 0.2, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
Toast {
id: infoToast
z: 1000
color: Qt.rgba(0.2, 0.8, 0.2, 0.9)
border.color: Qt.darker(infoToast.color, 1.3)
toastTextColor: "#FFFFFF"
}
ContextViewer {
id: contextViewer
width: Math.min(parent.width * 0.85, 800)
height: Math.min(parent.height * 0.85, 700)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
baseSystemPrompt: root.baseSystemPrompt
currentAgentRole: root.currentAgentRole
currentAgentRoleDescription: root.currentAgentRoleDescription
currentAgentRoleSystemPrompt: root.currentAgentRoleSystemPrompt
activeRules: root.activeRules
activeRulesCount: root.activeRulesCount
onOpenSettings: root.openSettings()
onOpenAgentRolesSettings: root.openAgentRolesSettings()
onOpenRulesFolder: root.openRulesFolder()
onRefreshRules: root.refreshRules()
onRuleSelected: function(index) {
contextViewer.selectedRuleContent = root.getRuleContent(index)
}
}
Connections {
target: root
function onLastErrorMessageChanged() {
if (root.lastErrorMessage.length > 0) {
errorToast.show(root.lastErrorMessage)
}
}
function onLastInfoMessageChanged() {
if (root.lastInfoMessage.length > 0) {
infoToast.show(root.lastInfoMessage)
}
}
function onOpenFilesChanged() {
if (fileMentionPopup.visible)
Qt.callLater(fileMentionPopup.refreshSearch)
}
}
FileMentionPopup {
id: fileMentionPopup
z: 999
width: Math.min(480, root.width - 20)
x: Math.max(5, Math.min(view.x + 5, root.width - width - 5))
y: view.y - height - 4
onSelectionRequested: root.applyMentionSelection()
onFileAttachRequested: function(filePaths) {
root.addFilesToAttachList(filePaths)
}
}
Component.onCompleted: {
messageInput.forceActiveFocus()
}
}

View File

@@ -0,0 +1,367 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import ChatView
import QtQuick.Controls
import QtQuick.Layouts
import UIControls
Rectangle {
id: root
property alias msgModel: msgCreator.model
property alias messageAttachments: attachmentsModel.model
property alias messageImages: imagesModel.model
property string chatFilePath: ""
property string textFontFamily: Qt.application.font.family
property string codeFontFamily: {
switch (Qt.platform.os) {
case "windows":
return "Consolas";
case "osx":
return "Menlo";
case "linux":
return "DejaVu Sans Mono";
default:
return "monospace";
}
}
property int textFontSize: Qt.application.font.pointSize
property int codeFontSize: Qt.application.font.pointSize
property int textFormat: 0
property bool isUserMessage: false
property int messageIndex: -1
signal resetChatToMessage(int index)
signal openFileRequested(string filePath)
height: msgColumn.implicitHeight + 10
radius: 8
color: isUserMessage ? palette.alternateBase
: palette.base
HoverHandler {
id: mouse
}
ColumnLayout {
id: msgColumn
x: 5
width: parent.width - x
anchors.verticalCenter: parent.verticalCenter
spacing: 5
Repeater {
id: msgCreator
delegate: Loader {
id: msgCreatorDelegate
// Fix me:
// why does `required property MessagePart modelData` not work?
required property var modelData
Layout.preferredWidth: root.width
sourceComponent: {
// If `required property MessagePart modelData` is used
// and conversion to MessagePart fails, you're left
// with a nullptr. This tests that to prevent crashing.
if(!modelData) {
return undefined;
}
switch(modelData.type) {
case MessagePartType.Text: return textComponent;
case MessagePartType.Code: return codeBlockComponent;
default: return textComponent;
}
}
Component {
id: textComponent
TextComponent {
itemData: msgCreatorDelegate.modelData
}
}
Component {
id: codeBlockComponent
CodeBlockComponent {
itemData: msgCreatorDelegate.modelData
}
}
}
}
Flow {
id: attachmentsFlow
Layout.fillWidth: true
visible: attachmentsModel.model && attachmentsModel.model.length > 0
leftPadding: 10
rightPadding: 10
spacing: 5
Repeater {
id: attachmentsModel
delegate: AttachmentComponent {
required property int index
required property var modelData
itemData: modelData
}
}
}
Flow {
id: imagesFlow
Layout.fillWidth: true
visible: imagesModel.model && imagesModel.model.length > 0
leftPadding: 10
rightPadding: 10
spacing: 10
Repeater {
id: imagesModel
delegate: ImageComponent {
required property int index
required property var modelData
itemData: modelData
}
}
}
}
Rectangle {
id: userMessageMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: "#92BD6C"
radius: root.radius
visible: root.isUserMessage
}
QoAButton {
id: stopButtonId
anchors {
right: parent.right
top: parent.top
}
icon {
source: "qrc:/qt/qml/ChatView/icons/undo-changes-button.svg"
height: 15
width: 15
}
visible: root.isUserMessage && mouse.hovered
onClicked: function() {
root.resetChatToMessage(root.messageIndex)
}
QoAToolTip {
visible: stopButtonId.hovered
text: qsTr("Reset chat to this message and edit")
delay: 500
}
}
component TextComponent : TextBlock {
required property var itemData
height: implicitHeight + 10
verticalAlignment: Text.AlignVCenter
leftPadding: 10
text: textFormat == Text.MarkdownText ? utils.getSafeMarkdownText(itemData.text)
: itemData.text
font.family: root.textFontFamily
font.pointSize: root.textFontSize
textFormat: {
if (root.textFormat == 0) {
return Text.MarkdownText
} else if (root.textFormat == 1) {
return Text.RichText
} else {
return Text.PlainText
}
}
onLinkActivated: function(link) {
if (link.startsWith("file://")) {
var filePath = link.replace(/^file:\/\//, "")
root.openFileRequested(filePath)
} else {
Qt.openUrlExternally(link)
}
}
ChatUtils {
id: utils
}
}
component CodeBlockComponent : CodeBlock {
id: codeblock
required property var itemData
anchors {
left: parent.left
leftMargin: 10
right: parent.right
rightMargin: 10
}
code: itemData.text
language: itemData.language
codeFontFamily: root.codeFontFamily
codeFontSize: root.codeFontSize
}
component AttachmentComponent : Rectangle {
required property var itemData
height: attachFileText.implicitHeight + 8
width: attachFileText.implicitWidth + 16
radius: 4
color: attachFileMouseArea.containsMouse ? Qt.lighter(palette.button, 1.1) : palette.button
border.width: 1
border.color: palette.mid
Behavior on color { ColorAnimation { duration: 100 } }
FileItem {
id: fileItem
filePath: itemData.filePath || ""
}
Text {
id: attachFileText
anchors.centerIn: parent
text: (itemData.fileName || "")
color: palette.buttonText
font.pointSize: root.textFontSize - 1
}
MouseArea {
id: attachFileMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (mouse.modifiers & Qt.ShiftModifier) {
fileItem.openFileInExternalEditor()
} else {
fileItem.openFileInEditor()
}
}
QoAToolTip {
visible: attachFileMouseArea.containsMouse
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
delay: 500
}
}
}
component ImageComponent : Rectangle {
required property var itemData
readonly property int maxImageWidth: Math.min(400, root.width - 40)
readonly property int maxImageHeight: 300
width: Math.min(imageDisplay.implicitWidth, maxImageWidth) + 16
height: imageDisplay.implicitHeight + fileNameText.implicitHeight + 16
radius: 4
color: imageMouseArea.containsMouse ? Qt.lighter(palette.base, 1.05) : palette.base
border.width: 1
border.color: palette.mid
Behavior on color { ColorAnimation { duration: 100 } }
FileItem {
id: imageFileItem
filePath: itemData.filePath || ""
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Image {
id: imageDisplay
Layout.alignment: Qt.AlignHCenter
Layout.maximumWidth: parent.parent.maxImageWidth
Layout.maximumHeight: parent.parent.maxImageHeight
source: itemData.imageUrl ? itemData.imageUrl : ""
sourceSize.width: parent.parent.maxImageWidth
sourceSize.height: parent.parent.maxImageHeight
fillMode: Image.PreserveAspectFit
cache: true
asynchronous: true
smooth: true
mipmap: true
BusyIndicator {
anchors.centerIn: parent
running: imageDisplay.status === Image.Loading
visible: running
}
Text {
anchors.centerIn: parent
text: qsTr("Failed to load image")
visible: imageDisplay.status === Image.Error
color: palette.placeholderText
}
}
Text {
id: fileNameText
Layout.fillWidth: true
text: itemData.fileName || ""
color: palette.text
font.pointSize: root.textFontSize - 1
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
}
}
MouseArea {
id: imageMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (mouse.modifiers & Qt.ShiftModifier) {
imageFileItem.openFileInExternalEditor()
} else {
imageFileItem.openFileInEditor()
}
}
QoAToolTip {
visible: imageMouseArea.containsMouse
text: qsTr("Click: Open in Qt Creator\nShift+Click: Open in System Editor")
delay: 500
}
}
}
}

View File

@@ -0,0 +1,157 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import ChatView
import UIControls
import Qt.labs.platform as Platform
Rectangle {
id: root
property string code: ""
property string language: ""
property bool expanded: false
property alias codeFontFamily: codeText.font.family
property alias codeFontSize: codeText.font.pointSize
readonly property real collapsedHeight: copyButton.height + 10
color: palette.alternateBase
border.color: root.color.hslLightness > 0.5 ? Qt.darker(root.color, 1.3)
: Qt.lighter(root.color, 1.3)
border.width: 2
radius: 4
implicitWidth: parent.width
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
ChatUtils {
id: utils
}
HoverHandler {
id: hoverHandler
enabled: true
}
MouseArea {
id: header
width: parent.width
height: root.collapsedHeight
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
spacing: 6
Text {
text: root.language ? qsTr("Code (%1)").arg(root.language) :
qsTr("Code")
font.pixelSize: 12
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
TextEdit {
id: codeText
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
text: root.code
readOnly: true
selectByMouse: true
color: parent.color.hslLightness > 0.5 ? "black" : "white"
wrapMode: Text.WordWrap
selectionColor: palette.highlight
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
}
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
onTriggered: {
const textToCopy = codeText.selectedText || root.code
utils.copyToClipboard(textToCopy)
}
}
Platform.MenuSeparator {}
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
QoAButton {
id: copyButton
anchors.right: parent.right
anchors.rightMargin: 5
y: 5
text: qsTr("Copy")
onClicked: {
utils.copyToClipboard(root.code)
text = qsTr("Copied")
copyTimer.start()
}
Timer {
id: copyTimer
interval: 2000
onTriggered: parent.text = qsTr("Copy")
}
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: root.collapsedHeight
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + codeText.implicitHeight + 10
}
}
]
}

View File

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

View File

@@ -0,0 +1,38 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import Qt.labs.platform as Platform
TextEdit {
id: root
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
cursorShape: root.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: qsTr("Copy")
enabled: root.selectedText.length > 0
onTriggered: root.copy()
}
Platform.MenuItem {
text: qsTr("Select All")
enabled: root.text.length > 0
onTriggered: root.selectAll()
}
}
}

View File

@@ -0,0 +1,167 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import Qt.labs.platform as Platform
Rectangle {
id: root
property string thinkingContent: ""
// property string signature: ""
property bool isRedacted: false
property bool expanded: false
property alias headerOpacity: headerRow.opacity
radius: 6
color: palette.base
clip: true
Behavior on implicitHeight {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
MouseArea {
id: header
width: parent.width
height: headerRow.height + 10
cursorShape: Qt.PointingHandCursor
onClicked: root.expanded = !root.expanded
Row {
id: headerRow
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 10
}
width: parent.width
spacing: 8
Text {
text: root.isRedacted ? qsTr("Thinking (Redacted)")
: qsTr("Thinking")
font.pixelSize: 13
font.bold: true
color: palette.text
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.mid
}
}
}
Column {
id: contentColumn
anchors {
left: parent.left
right: parent.right
top: header.bottom
margins: 10
}
spacing: 8
Text {
visible: root.isRedacted
width: parent.width
text: qsTr("Thinking content was redacted by safety systems")
font.pixelSize: 11
font.italic: true
color: Qt.rgba(0.8, 0.4, 0.4, 1.0)
wrapMode: Text.WordWrap
}
TextEdit {
id: thinkingText
visible: !root.isRedacted
width: parent.width
text: root.thinkingContent
readOnly: true
selectByMouse: true
color: palette.text
wrapMode: Text.WordWrap
font.family: "monospace"
font.pixelSize: 11
selectionColor: palette.highlight
}
// Rectangle {
// visible: root.signature.length > 0 && root.expanded
// width: parent.width
// height: signatureText.height + 10
// color: palette.alternateBase
// radius: 4
// Text {
// id: signatureText
// anchors {
// left: parent.left
// right: parent.right
// verticalCenter: parent.verticalCenter
// margins: 5
// }
// text: qsTr("Signature: %1").arg(root.signature.substring(0, Math.min(40, root.signature.length)) + "...")
// font.pixelSize: 9
// font.family: "monospace"
// color: palette.mid
// elide: Text.ElideRight
// }
// }
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: contextMenu.open()
propagateComposedEvents: true
}
Platform.Menu {
id: contextMenu
Platform.MenuItem {
text: root.expanded ? qsTr("Collapse") : qsTr("Expand")
onTriggered: root.expanded = !root.expanded
}
}
Rectangle {
id: thinkingMarker
anchors.verticalCenter: parent.verticalCenter
width: 3
height: root.height - root.radius
color: root.isRedacted ? Qt.rgba(0.8, 0.3, 0.3, 0.9)
: (root.color.hslLightness > 0.5 ? Qt.darker(palette.alternateBase, 1.3)
: Qt.lighter(palette.alternateBase, 1.3))
radius: root.radius
}
states: [
State {
when: !root.expanded
PropertyChanges {
target: root
implicitHeight: header.height
}
},
State {
when: root.expanded
PropertyChanges {
target: root
implicitHeight: header.height + contentColumn.height + 20
}
}
]
}

View File

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

View File

@@ -0,0 +1,145 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
import UIControls
Flow {
id: root
property alias attachedFilesModel: attachRepeater.model
property color accentColor: palette.mid
property string iconPath
signal removeFileFromListByIndex(index: int)
spacing: 5
leftPadding: 5
rightPadding: 5
topPadding: attachRepeater.model.length > 0 ? 2 : 0
bottomPadding: attachRepeater.model.length > 0 ? 2 : 0
Repeater {
id: attachRepeater
delegate: FileItem {
id: fileItem
required property int index
required property string modelData
filePath: modelData
height: 30
width: contentRow.width + 10
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()
}
}
QoAToolTip {
visible: mouse.containsMouse
delay: 500
text: "Click: Open in Qt Creator\nShift+Click: Open in external editor\nCtrl+Click / Middle Click: Remove"
}
}
Menu {
id: contextMenu
MenuItem {
text: "Open in Qt Creator"
onTriggered: fileItem.openFileInEditor()
}
MenuItem {
text: "Open in External Editor"
onTriggered: fileItem.openFileInExternalEditor()
}
MenuSeparator {}
MenuItem {
text: "Remove"
onTriggered: root.removeFileFromListByIndex(fileItem.index)
}
}
Row {
id: contentRow
spacing: 5
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.leftMargin: 5
Image {
id: icon
anchors.verticalCenter: parent.verticalCenter
source: root.iconPath
sourceSize.width: 8
sourceSize.height: 15
}
Text {
id: fileNameText
anchors.verticalCenter: parent.verticalCenter
color: palette.buttonText
text: {
const parts = modelData.split('/');
return parts[parts.length - 1];
}
}
MouseArea {
id: closeButton
anchors.verticalCenter: parent.verticalCenter
width: closeIcon.width + 5
height: closeButton.width + 5
onClicked: root.removeFileFromListByIndex(index)
Image {
id: closeIcon
anchors.centerIn: parent
source: palette.window.hslLightness > 0.5 ? "qrc:/qt/qml/ChatView/icons/close-dark.svg"
: "qrc:/qt/qml/ChatView/icons/close-light.svg"
width: 6
height: 6
}
}
}
}
}
}

View File

@@ -0,0 +1,153 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import ChatView
import UIControls
Rectangle {
id: root
property alias sendButton: sendButtonId
property alias syncOpenFiles: syncOpenFilesId
property alias attachFiles: attachFilesId
property alias attachImages: attachImagesId
property alias linkFiles: linkFilesId
property alias compressButton: compressButtonId
property alias cancelCompressButton: cancelCompressButtonId
property bool isCompressing: false
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
RowLayout {
id: bottomBar
anchors {
left: parent.left
leftMargin: 5
right: parent.right
rightMargin: 5
verticalCenter: parent.verticalCenter
}
spacing: 10
QoAButton {
id: attachFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/attach-file-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Attach file to message")
}
QoAButton {
id: attachImagesId
icon {
source: "qrc:/qt/qml/ChatView/icons/image-dark.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Attach image to message")
}
QoAButton {
id: linkFilesId
icon {
source: "qrc:/qt/qml/ChatView/icons/link-file-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Link file to context")
}
CheckBox {
id: syncOpenFilesId
text: qsTr("Sync open files")
ToolTip.visible: syncOpenFilesId.hovered
ToolTip.text: qsTr("Automatically synchronize currently opened files with the model context")
}
Item {
Layout.fillWidth: true
}
Row {
id: compressingRow
visible: root.isCompressing
spacing: 6
BusyIndicator {
id: compressBusyIndicator
anchors.verticalCenter: parent.verticalCenter
running: root.isCompressing
width: 16
height: 16
}
Text {
text: qsTr("Compressing...")
anchors.verticalCenter: parent.verticalCenter
color: palette.text
font.pixelSize: 12
}
QoAButton {
id: cancelCompressButtonId
anchors.verticalCenter: parent.verticalCenter
text: qsTr("Cancel")
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Cancel compression")
}
}
QoAButton {
id: compressButtonId
visible: !root.isCompressing
text: qsTr("Compress")
icon {
source: "qrc:/qt/qml/ChatView/icons/compress-icon.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Compress chat (create summarized copy using LLM)")
}
QoAButton {
id: sendButtonId
icon {
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
}
}
}

View File

@@ -0,0 +1,542 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Basic as QQC
import UIControls
import ChatView
Popup {
id: root
property string baseSystemPrompt
property string currentAgentRole
property string currentAgentRoleDescription
property string currentAgentRoleSystemPrompt
property var activeRules
property int activeRulesCount
property string selectedRuleContent
signal openSettings()
signal openAgentRolesSettings()
signal openRulesFolder()
signal refreshRules()
signal ruleSelected(int index)
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ChatUtils {
id: utils
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 10
Text {
text: qsTr("Chat Context")
font.pixelSize: 16
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Refresh")
onClicked: root.refreshRules()
}
QoAButton {
text: qsTr("Close")
onClicked: root.close()
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Flickable {
id: mainFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: sectionsColumn.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
ColumnLayout {
id: sectionsColumn
width: mainFlickable.width
spacing: 8
CollapsibleSection {
id: systemPromptSection
Layout.fillWidth: true
title: qsTr("Base System Prompt")
badge: root.baseSystemPrompt.length > 0 ? qsTr("Active") : qsTr("Empty")
badgeColor: root.baseSystemPrompt.length > 0 ? Qt.rgba(0.2, 0.6, 0.3, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 5
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(systemPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
Flickable {
id: systemPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: systemPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: systemPromptText
width: systemPromptFlickable.width
text: root.baseSystemPrompt.length > 0 ? root.baseSystemPrompt : qsTr("No system prompt configured")
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: root.baseSystemPrompt.length > 0 ? palette.text : palette.mid
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: systemPromptFlickable.contentHeight > systemPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.baseSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.baseSystemPrompt)
}
QoAButton {
text: qsTr("Edit in Settings")
onClicked: {
root.openSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: agentRoleSection
Layout.fillWidth: true
title: qsTr("Agent Role")
badge: root.currentAgentRole
badgeColor: root.currentAgentRoleSystemPrompt.length > 0 ? Qt.rgba(0.3, 0.4, 0.7, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
Text {
text: root.currentAgentRoleDescription
font.pixelSize: 11
font.italic: true
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleDescription.length > 0
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: Math.min(Math.max(agentPromptText.implicitHeight + 16, 50), 200)
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
visible: root.currentAgentRoleSystemPrompt.length > 0
Flickable {
id: agentPromptFlickable
anchors.fill: parent
anchors.margins: 8
contentHeight: agentPromptText.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: agentPromptText
width: agentPromptFlickable.width
text: root.currentAgentRoleSystemPrompt
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: agentPromptFlickable.contentHeight > agentPromptFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
Text {
text: qsTr("No role selected. Using base system prompt only.")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
visible: root.currentAgentRoleSystemPrompt.length === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Copy")
enabled: root.currentAgentRoleSystemPrompt.length > 0
onClicked: utils.copyToClipboard(root.currentAgentRoleSystemPrompt)
}
QoAButton {
text: qsTr("Manage Roles")
onClicked: {
root.openAgentRolesSettings()
root.close()
}
}
}
}
}
CollapsibleSection {
id: projectRulesSection
Layout.fillWidth: true
title: qsTr("Project Rules")
badge: root.activeRulesCount > 0 ? qsTr("%1 active").arg(root.activeRulesCount) : qsTr("None")
badgeColor: root.activeRulesCount > 0 ? Qt.rgba(0.6, 0.5, 0.2, 1.0) : palette.mid
sectionContent: ColumnLayout {
spacing: 8
SplitView {
Layout.fillWidth: true
Layout.preferredHeight: 220
orientation: Qt.Horizontal
visible: root.activeRulesCount > 0
Rectangle {
SplitView.minimumWidth: 120
SplitView.preferredWidth: 180
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
Text {
text: qsTr("Rules (%1)").arg(rulesList.count)
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
ListView {
id: rulesList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: root.activeRules
currentIndex: 0
boundsBehavior: Flickable.StopAtBounds
delegate: ItemDelegate {
required property var modelData
required property int index
width: ListView.view.width
height: ruleItemContent.implicitHeight + 8
highlighted: ListView.isCurrentItem
background: Rectangle {
color: {
if (parent.highlighted)
return palette.highlight
if (parent.hovered)
return Qt.tint(palette.base, Qt.rgba(0, 0, 0, 0.05))
return "transparent"
}
radius: 2
}
contentItem: ColumnLayout {
id: ruleItemContent
spacing: 2
Text {
text: modelData.fileName
font.pixelSize: 10
color: parent.parent.highlighted ? palette.highlightedText : palette.text
elide: Text.ElideMiddle
Layout.fillWidth: true
}
Text {
text: modelData.category
font.pixelSize: 9
color: parent.parent.highlighted ? palette.highlightedText : palette.mid
Layout.fillWidth: true
}
}
onClicked: {
rulesList.currentIndex = index
root.ruleSelected(index)
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: rulesList.contentHeight > rulesList.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
Rectangle {
SplitView.fillWidth: true
SplitView.minimumWidth: 200
color: palette.base
border.color: palette.mid
border.width: 1
radius: 2
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 5
RowLayout {
Layout.fillWidth: true
spacing: 5
Text {
text: qsTr("Content")
font.pixelSize: 11
font.bold: true
color: palette.text
Layout.fillWidth: true
}
QoAButton {
text: qsTr("Copy")
enabled: root.selectedRuleContent.length > 0
onClicked: utils.copyToClipboard(root.selectedRuleContent)
}
}
Flickable {
id: ruleContentFlickable
Layout.fillWidth: true
Layout.fillHeight: true
contentHeight: ruleContentArea.implicitHeight
clip: true
boundsBehavior: Flickable.StopAtBounds
TextEdit {
id: ruleContentArea
width: ruleContentFlickable.width
text: root.selectedRuleContent
readOnly: true
selectByMouse: true
wrapMode: Text.WordWrap
selectionColor: palette.highlight
color: palette.text
font.family: "monospace"
font.pixelSize: 11
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: ruleContentFlickable.contentHeight > ruleContentFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
}
}
}
Text {
text: qsTr("No project rules found.\nCreate .md files in .qodeassist/rules/common/ or .qodeassist/rules/chat/")
font.pixelSize: 11
color: palette.mid
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
visible: root.activeRulesCount === 0
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
QoAButton {
text: qsTr("Open Rules Folder")
onClicked: root.openRulesFolder()
}
}
}
}
}
QQC.ScrollBar.vertical: QQC.ScrollBar {
policy: mainFlickable.contentHeight > mainFlickable.height ? QQC.ScrollBar.AsNeeded : QQC.ScrollBar.AlwaysOff
}
}
Rectangle {
Layout.fillWidth: true
height: 1
color: palette.mid
}
Text {
text: qsTr("Final prompt: Base System Prompt + Agent Role + Project Info + Project Rules + Linked Files")
font.pixelSize: 9
color: palette.mid
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
}
component CollapsibleSection: ColumnLayout {
id: sectionRoot
property string title
property string badge
property color badgeColor: palette.mid
property Component sectionContent: null
property bool expanded: false
spacing: 0
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 32
color: sectionMouseArea.containsMouse ? Qt.tint(palette.button, Qt.rgba(0, 0, 0, 0.05)) : palette.button
border.color: palette.mid
border.width: 1
radius: 2
MouseArea {
id: sectionMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: sectionRoot.expanded = !sectionRoot.expanded
}
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
text: sectionRoot.expanded ? "▼" : "▶"
font.pixelSize: 10
color: palette.text
}
Text {
text: sectionRoot.title
font.pixelSize: 12
font.bold: true
color: palette.text
Layout.fillWidth: true
}
Rectangle {
implicitWidth: badgeText.implicitWidth + 12
implicitHeight: 18
color: sectionRoot.badgeColor
radius: 3
Text {
id: badgeText
anchors.centerIn: parent
text: sectionRoot.badge
font.pixelSize: 10
color: "#FFFFFF"
}
}
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.leftMargin: 12
Layout.topMargin: 8
Layout.bottomMargin: 4
sourceComponent: sectionRoot.sectionContent
visible: sectionRoot.expanded
active: sectionRoot.expanded
}
}
onOpened: {
if (root.activeRulesCount > 0) {
root.ruleSelected(0)
}
}
}

View File

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

View File

@@ -0,0 +1,151 @@
// Copyright (C) 2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FileMentionItem {
id: root
signal selectionRequested()
visible: searchResults.length > 0
height: Math.min(searchResults.length * 36, 36 * 6) + 2
onCurrentIndexChanged: {
listView.positionViewAtIndex(root.currentIndex, ListView.Contain)
}
Rectangle {
id: background
anchors.fill: parent
color: palette.window
border.color: palette.mid
border.width: 1
radius: 4
}
ListView {
id: listView
anchors.fill: parent
anchors.margins: 1
model: root.searchResults
currentIndex: root.currentIndex
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
}
delegate: Rectangle {
id: delegateItem
required property int index
required property var modelData
readonly property bool isProject: modelData.isProject === true
readonly property bool isOpen: modelData.isOpen === true
readonly property string fileName: {
if (isProject)
return modelData.projectName
const parts = modelData.relativePath.split('/')
return parts[parts.length - 1]
}
width: listView.width
height: 36
color: index === root.currentIndex
? palette.highlight
: (hoverArea.containsMouse
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.25)
: "transparent")
RowLayout {
anchors.fill: parent
anchors.leftMargin: 10
anchors.rightMargin: 10
spacing: 8
Item {
Layout.preferredWidth: 18
Layout.preferredHeight: 18
Rectangle {
anchors.fill: parent
radius: 3
visible: delegateItem.isProject || delegateItem.isOpen
color: {
if (delegateItem.index === root.currentIndex)
return Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.2)
if (delegateItem.isProject)
return Qt.rgba(palette.highlight.r,
palette.highlight.g,
palette.highlight.b, 0.3)
return Qt.rgba(0.2, 0.7, 0.4, 0.3)
}
Text {
anchors.centerIn: parent
text: delegateItem.isProject ? "P" : "O"
font.bold: true
font.pixelSize: 10
color: {
if (delegateItem.index === root.currentIndex)
return palette.highlightedText
if (delegateItem.isProject)
return palette.highlight
return Qt.rgba(0.1, 0.6, 0.3, 1.0)
}
}
}
}
Text {
Layout.preferredWidth: 160
text: delegateItem.fileName
color: delegateItem.index === root.currentIndex
? palette.highlightedText
: (delegateItem.isProject ? palette.highlight : palette.text)
font.bold: true
font.italic: delegateItem.isProject
elide: Text.ElideRight
}
Text {
Layout.fillWidth: true
text: delegateItem.isProject
? "→"
: (delegateItem.modelData.projectName + " / " + delegateItem.modelData.relativePath)
color: delegateItem.index === root.currentIndex
? (delegateItem.isProject
? palette.highlightedText
: Qt.rgba(palette.highlightedText.r,
palette.highlightedText.g,
palette.highlightedText.b, 0.7))
: palette.mid
font.pixelSize: delegateItem.isProject ? 12 : 11
elide: Text.ElideLeft
horizontalAlignment: delegateItem.isProject ? Text.AlignLeft : Text.AlignRight
}
}
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
root.currentIndex = delegateItem.index
root.selectionRequested()
}
onEntered: root.currentIndex = delegateItem.index
}
}
}
}

View File

@@ -0,0 +1,275 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Controls
Item {
id: root
signal filesDroppedToAttach(var urlStrings)
signal filesDroppedToLink(var urlStrings)
property string activeZone: ""
property int filesCount: 0
property bool isDragActive: false
Item {
id: splitDropOverlay
anchors.fill: parent
visible: false
z: 999
opacity: 0
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(palette.shadow.r, palette.shadow.g, palette.shadow.b, 0.6)
}
Rectangle {
anchors {
top: parent.top
horizontalCenter: parent.horizontalCenter
topMargin: 30
}
width: fileCountText.width + 40
height: 50
color: Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.9)
radius: 25
visible: root.filesCount > 0
Text {
id: fileCountText
anchors.centerIn: parent
text: qsTr("%n file(s) to drop", "", root.filesCount)
font.pixelSize: 16
font.bold: true
color: palette.highlightedText
}
}
Rectangle {
id: leftZone
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
color: root.activeZone === "left"
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
border.width: root.activeZone === "left" ? 3 : 2
border.color: root.activeZone === "left"
? palette.highlight
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
Column {
anchors.centerIn: parent
spacing: 15
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Attach")
font.pixelSize: 24
font.bold: true
color: root.activeZone === "left" ? palette.highlightedText : palette.text
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Images & Text Files")
font.pixelSize: 14
color: root.activeZone === "left" ? palette.highlightedText : palette.text
opacity: 0.8
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("(for one-time use)")
font.pixelSize: 12
font.italic: true
color: root.activeZone === "left" ? palette.highlightedText : palette.text
opacity: 0.6
}
}
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.width { NumberAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
}
Rectangle {
id: rightZone
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
color: root.activeZone === "right"
? Qt.rgba(palette.highlight.r, palette.highlight.g, palette.highlight.b, 0.3)
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.15)
border.width: root.activeZone === "right" ? 3 : 2
border.color: root.activeZone === "right"
? palette.highlight
: Qt.rgba(palette.mid.r, palette.mid.g, palette.mid.b, 0.5)
Column {
anchors.centerIn: parent
spacing: 15
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("LINK")
font.pixelSize: 24
font.bold: true
color: root.activeZone === "right" ? palette.highlightedText : palette.text
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("Text Files")
font.pixelSize: 14
color: root.activeZone === "right" ? palette.highlightedText : palette.text
opacity: 0.8
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: qsTr("(added to context)")
font.pixelSize: 12
font.italic: true
color: root.activeZone === "right" ? palette.highlightedText : palette.text
opacity: 0.6
}
}
Behavior on color { ColorAnimation { duration: 150 } }
Behavior on border.width { NumberAnimation { duration: 150 } }
Behavior on border.color { ColorAnimation { duration: 150 } }
}
Rectangle {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
}
width: 2
color: palette.mid
opacity: 0.4
}
MouseArea {
id: leftDropArea
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
hoverEnabled: true
onEntered: {
root.activeZone = "left"
}
}
MouseArea {
id: rightDropArea
anchors {
right: parent.right
top: parent.top
bottom: parent.bottom
}
width: parent.width / 2
hoverEnabled: true
onEntered: {
root.activeZone = "right"
}
}
}
DropArea {
id: globalDropArea
anchors.fill: parent
onEntered: (drag) => {
if (drag.hasUrls) {
root.isDragActive = true
root.filesCount = drag.urls.length
splitDropOverlay.visible = true
splitDropOverlay.opacity = 1
root.activeZone = ""
}
}
onExited: {
root.isDragActive = false
root.filesCount = 0
splitDropOverlay.opacity = 0
Qt.callLater(function() {
if (!root.isDragActive) {
splitDropOverlay.visible = false
root.activeZone = ""
}
})
}
onPositionChanged: (drag) => {
if (drag.hasUrls) {
root.activeZone = drag.x < globalDropArea.width / 2 ? "left" : "right"
}
}
onDropped: (drop) => {
const targetZone = root.activeZone
root.isDragActive = false
root.filesCount = 0
splitDropOverlay.opacity = 0
Qt.callLater(function() {
splitDropOverlay.visible = false
root.activeZone = ""
})
if (!drop.hasUrls || drop.urls.length === 0) {
return
}
var urlStrings = []
for (var i = 0; i < drop.urls.length; i++) {
var urlString = drop.urls[i].toString()
if (urlString.startsWith("file://") || urlString.indexOf("://") === -1) {
urlStrings.push(urlString)
}
}
if (urlStrings.length === 0) {
return
}
drop.accept(Qt.CopyAction)
if (targetZone === "right") {
root.filesDroppedToLink(urlStrings)
} else {
root.filesDroppedToAttach(urlStrings)
}
}
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
Rectangle {
id: root
property alias toastTextItem: textItem
property alias toastTextColor: textItem.color
property string errorText: ""
property int displayDuration: 7000
width: Math.min(parent.width - 40, textItem.implicitWidth + radius)
height: visible ? (textItem.implicitHeight + 12) : 0
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: 10
color: "#d32f2f"
radius: height / 2
border.color: "#b71c1c"
border.width: 1
visible: false
opacity: 0
TextEdit {
id: textItem
anchors.centerIn: parent
anchors.margins: 6
text: root.errorText
color: palette.text
font.pixelSize: 13
wrapMode: TextEdit.Wrap
width: Math.min(implicitWidth, root.parent.width - 60)
horizontalAlignment: TextEdit.AlignHCenter
readOnly: true
selectByMouse: true
selectByKeyboard: true
selectionColor: "#b71c1c"
}
function show(message) {
errorText = message
visible = true
showAnimation.start()
hideTimer.restart()
}
function hide() {
hideAnimation.start()
}
NumberAnimation {
id: showAnimation
target: root
property: "opacity"
from: 0
to: 1
duration: 200
easing.type: Easing.OutQuad
}
NumberAnimation {
id: hideAnimation
target: root
property: "opacity"
from: 1
to: 0
duration: 200
easing.type: Easing.InQuad
onFinished: root.visible = false
}
Timer {
id: hideTimer
interval: root.displayDuration
running: false
repeat: false
onTriggered: root.hide()
}
}

View File

@@ -0,0 +1,280 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import ChatView
import UIControls
Rectangle {
id: root
property alias saveButton: saveButtonId
property alias loadButton: loadButtonId
property alias clearButton: clearButtonId
property alias tokensBadge: tokensBadgeId
property alias recentPath: recentPathId
property alias openChatHistory: openChatHistoryId
property alias pinButton: pinButtonId
property alias contextButton: contextButtonId
property alias toolsButton: toolsButtonId
property alias thinkingMode: thinkingModeId
property alias settingsButton: settingsButtonId
property alias configSelector: configSelectorId
property alias roleSelector: roleSelector
color: palette.window.hslLightness > 0.5 ?
Qt.darker(palette.window, 1.1) :
Qt.lighter(palette.window, 1.1)
Flow {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
margins: 5
}
spacing: 10
Row {
id: firstRow
spacing: 10
QoAButton {
id: pinButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/window-lock.svg"
: "qrc:/qt/qml/ChatView/icons/window-unlock.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: checked ? qsTr("Unpin chat window")
: qsTr("Pin chat window to the top")
}
QoAComboBox {
id: configSelectorId
implicitHeight: 25
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch saved AI configuration")
}
QoAComboBox {
id: roleSelector
implicitHeight: 25
model: []
currentIndex: 0
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Switch agent role (different system prompts)")
}
}
Row {
spacing: 10
QoAButton {
id: toolsButtonId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/tools-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/tools-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: {
if (!toolsButtonId.enabled) {
return qsTr("Tools are disabled in General Settings")
}
return checked
? qsTr("Tools enabled: AI can use tools to read files, search project, and build code")
: qsTr("Tools disabled: Simple conversation without tool access")
}
}
QoAButton {
id: thinkingModeId
anchors.verticalCenter: parent.verticalCenter
checkable: true
opacity: enabled ? 1.0 : 0.2
icon {
source: checked ? "qrc:/qt/qml/ChatView/icons/thinking-icon-on.svg"
: "qrc:/qt/qml/ChatView/icons/thinking-icon-off.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: enabled ? (checked ? qsTr("Thinking Mode enabled (Check model list support it)")
: qsTr("Thinking Mode disabled"))
: qsTr("Thinking Mode is not available for this provider")
}
QoAButton {
id: settingsButtonId
anchors.verticalCenter: parent.verticalCenter
icon {
source: "qrc:/qt/qml/ChatView/icons/settings-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Open Chat Assistant Settings")
}
QoASeparator {
anchors.verticalCenter: parent.verticalCenter
}
}
Item {
height: firstRow.height
width: recentPathId.width
Text {
id: recentPathId
anchors.verticalCenter: parent.verticalCenter
width: Math.min(implicitWidth, root.width)
elide: Text.ElideMiddle
color: palette.text
font.pixelSize: 12
MouseArea {
anchors.fill: parent
hoverEnabled: true
ToolTip.visible: containsMouse
ToolTip.delay: 500
ToolTip.text: recentPathId.text
}
}
}
RowLayout {
id: secondRow
Layout.preferredWidth: root.width
Layout.preferredHeight: firstRow.height
spacing: 10
QoAButton {
id: saveButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/save-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Save chat to *.json file")
}
QoAButton {
id: loadButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/load-chat-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Load chat from *.json file")
}
QoAButton {
id: openChatHistoryId
icon {
source: "qrc:/qt/qml/ChatView/icons/file-in-system.svg"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Show in system")
}
QoASeparator {}
QoAButton {
id: contextButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/context-icon.svg"
color: palette.window.hslLightness > 0.5 ? "#000000" : "#FFFFFF"
height: 15
width: 15
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("View chat context (system prompt, role, rules)")
}
Badge {
id: tokensBadgeId
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Current amount tokens in chat and LLM limit threshold")
}
QoASeparator {}
QoAButton {
id: clearButtonId
icon {
source: "qrc:/qt/qml/ChatView/icons/clean-icon-dark.svg"
height: 15
width: 8
}
ToolTip.visible: hovered
ToolTip.delay: 250
ToolTip.text: qsTr("Clean chat")
}
}
}
}

231
CodeHandler.cpp Normal file
View File

@@ -0,0 +1,231 @@
// Copyright (C) 2024-2026 Petr Mironychev
// Copyright (C) 2025 Povilas Kanapickas <povilas@radix.lt>
// SPDX-License-Identifier: GPL-3.0-or-later
#include "CodeHandler.hpp"
#include <settings/CodeCompletionSettings.hpp>
#include <QFileInfo>
#include <QHash>
namespace QodeAssist {
struct LanguageProperties
{
QString name;
QString commentStyle;
QVector<QString> namesFromModel;
QVector<QString> fileExtensions;
};
const QVector<LanguageProperties> customLanguagesFromSettings()
{
QVector<LanguageProperties> customLanguages;
const QStringList customLanguagesList = Settings::codeCompletionSettings().customLanguages();
for (const QString &entry : customLanguagesList) {
if (entry.trimmed().isEmpty()) {
continue;
}
QStringList parts = entry.split(',');
if (parts.size() < 4) {
continue;
}
QString name = parts[0].trimmed();
QString commentStyle = parts[1].trimmed();
QStringList modelNamesList = parts[2].trimmed().split(' ', Qt::SkipEmptyParts);
QStringList extensionsList = parts[3].trimmed().split(' ', Qt::SkipEmptyParts);
if (!name.isEmpty() && !commentStyle.isEmpty() && !modelNamesList.isEmpty()
&& !extensionsList.isEmpty()) {
QVector<QString> modelNames;
for (const auto &modelName : modelNamesList) {
modelNames.append(modelName);
}
QVector<QString> extensions;
for (const auto &ext : extensionsList) {
extensions.append(ext);
}
customLanguages.append({name, commentStyle, modelNames, extensions});
}
}
return customLanguages;
}
const QVector<LanguageProperties> &getKnownLanguages()
{
static QVector<LanguageProperties> knownLanguages = {
{"python", "#", {"python", "py"}, {"py"}},
{"lua", "--", {"lua"}, {"lua"}},
{"js", "//", {"js", "javascript"}, {"js", "jsx"}},
{"ts", "//", {"ts", "typescript"}, {"ts", "tsx"}},
{"c-like", "//", {"c", "c++", "cpp"}, {"c", "h", "cpp", "hpp"}},
{"java", "//", {"java"}, {"java"}},
{"c#", "//", {"cs", "csharp"}, {"cs"}},
{"php", "//", {"php"}, {"php"}},
{"ruby", "#", {"rb", "ruby"}, {"rb"}},
{"go", "//", {"go"}, {"go"}},
{"swift", "//", {"swift"}, {"swift"}},
{"kotlin", "//", {"kt", "kotlin"}, {"kt", "kotlin"}},
{"scala", "//", {"scala"}, {"scala"}},
{"r", "#", {"r"}, {"r"}},
{"shell", "#", {"shell", "bash", "sh"}, {"sh", "bash"}},
{"perl", "#", {"pl", "perl"}, {"pl"}},
{"hs", "--", {"hs", "haskell"}, {"hs"}},
{"qml", "//", {"qml"}, {"qml"}},
};
knownLanguages.append(customLanguagesFromSettings());
return knownLanguages;
}
bool CodeHandler::hasCodeBlocks(const QString &text)
{
QStringList lines = text.split('\n');
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
return true;
}
}
return false;
}
static QHash<QString, QString> buildLanguageToCommentPrefixMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
result[languageProps.name] = languageProps.commentStyle;
}
return result;
}
static QHash<QString, QString> buildExtensionToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &extension : languageProps.fileExtensions) {
result[extension] = languageProps.name;
}
}
return result;
}
static QHash<QString, QString> buildModelLanguageNameToLanguageMap()
{
QHash<QString, QString> result;
for (const auto &languageProps : getKnownLanguages()) {
for (const auto &nameFromModel : languageProps.namesFromModel) {
result[nameFromModel] = languageProps.name;
}
}
return result;
}
QString CodeHandler::processText(QString text, QString currentFilePath)
{
QString result;
QStringList lines = text.split('\n');
bool inCodeBlock = false;
QString pendingComments;
auto currentFileExtension = QFileInfo(currentFilePath).suffix();
auto currentLanguage = detectLanguageFromExtension(currentFileExtension);
auto addPendingCommentsIfAny = [&]() {
if (pendingComments.isEmpty()) {
return;
}
QStringList commentLines = pendingComments.split('\n');
QString commentPrefix = getCommentPrefix(currentLanguage);
for (const QString &commentLine : commentLines) {
if (!commentLine.trimmed().isEmpty()) {
result += commentPrefix + " " + commentLine.trimmed() + "\n";
} else {
result += "\n";
}
}
pendingComments.clear();
};
for (const QString &line : lines) {
if (line.trimmed().startsWith("```")) {
if (!inCodeBlock) {
auto lineLanguage = detectLanguageFromLine(line);
if (!lineLanguage.isEmpty()) {
currentLanguage = lineLanguage;
}
addPendingCommentsIfAny();
if (lineLanguage.isEmpty()) {
// language not detected, so add direct output from model, if any
result += line.trimmed().mid(3) + "\n"; // add the remainder of line after ```
}
}
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) {
result += line + "\n";
} else {
QString trimmed = line.trimmed();
if (!trimmed.isEmpty()) {
pendingComments += trimmed + "\n";
} else {
pendingComments += "\n";
}
}
}
addPendingCommentsIfAny();
return result;
}
QString CodeHandler::getCommentPrefix(const QString &language)
{
static const auto commentPrefixes = buildLanguageToCommentPrefixMap();
return commentPrefixes.value(language, "//");
}
QString CodeHandler::detectLanguageFromLine(const QString &line)
{
static const auto modelNameToLanguage = buildModelLanguageNameToLanguageMap();
return modelNameToLanguage.value(line.trimmed().mid(3).trimmed(), "");
}
QString CodeHandler::detectLanguageFromExtension(const QString &extension)
{
static const auto extensionToLanguage = buildExtensionToLanguageMap();
return extensionToLanguage.value(extension.toLower(), "");
}
const QRegularExpression &CodeHandler::getFullCodeBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialStartBlockRegex()
{
static const QRegularExpression
regex(R"(```[\w\s]*\n([\s\S]*?)$)", QRegularExpression::MultilineOption);
return regex;
}
const QRegularExpression &CodeHandler::getPartialEndBlockRegex()
{
static const QRegularExpression regex(R"(^([\s\S]*?)```)", QRegularExpression::MultilineOption);
return regex;
}
} // namespace QodeAssist

40
CodeHandler.hpp Normal file
View File

@@ -0,0 +1,40 @@
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include <QRegularExpression>
#include <QString>
namespace QodeAssist {
class CodeHandler
{
public:
static QString processText(QString text, QString currentFileName);
/**
* Detects language from line, or returns empty string if this was not possible
*/
static QString detectLanguageFromLine(const QString &line);
/**
* Detects language file name, or returns empty string if this was not possible
*/
static QString detectLanguageFromExtension(const QString &extension);
/**
* Detects if text contains code blocks, or returns false if this was not possible
*/
static bool hasCodeBlocks(const QString &text);
private:
static QString getCommentPrefix(const QString &language);
static const QRegularExpression &getFullCodeBlockRegex();
static const QRegularExpression &getPartialStartBlockRegex();
static const QRegularExpression &getPartialEndBlockRegex();
};
} // namespace QodeAssist

235
ConfigurationManager.cpp Normal file
View File

@@ -0,0 +1,235 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ConfigurationManager.hpp"
#include <settings/ButtonAspect.hpp>
#include <QTimer>
#include "QodeAssisttr.h"
namespace QodeAssist {
ConfigurationManager &ConfigurationManager::instance()
{
static ConfigurationManager instance;
return instance;
}
void ConfigurationManager::init()
{
setupConnections();
updateAllTemplateDescriptions();
checkAllTemplate();
}
void ConfigurationManager::updateTemplateDescription(const Utils::StringAspect &templateAspect)
{
PluginLLMCore::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)
{
PluginLLMCore::PromptTemplate *templ = m_templateManger.getFimTemplateByName(templateAspect.value());
if (templ->name() == templateAspect.value())
return;
if (&templateAspect == &m_generalSettings.ccTemplate) {
m_generalSettings.ccTemplate.setValue(templ->name());
} else if (&templateAspect == &m_generalSettings.caTemplate) {
m_generalSettings.caTemplate.setValue(templ->name());
}
}
void ConfigurationManager::checkAllTemplate()
{
checkTemplate(m_generalSettings.ccTemplate);
checkTemplate(m_generalSettings.caTemplate);
}
ConfigurationManager::ConfigurationManager(QObject *parent)
: QObject(parent)
, m_generalSettings(Settings::generalSettings())
, m_providersManager(PluginLLMCore::ProvidersManager::instance())
, m_templateManger(PluginLLMCore::PromptTemplateManager::instance())
{}
void ConfigurationManager::setupConnections()
{
using Config = ConfigurationManager;
using Button = ButtonAspect;
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);
connect(&m_generalSettings.ccPreset1SetUrl, &Button::clicked, this, &Config::selectUrl);
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()
{
const auto providersList = m_providersManager.providersNames();
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
auto &targetSettings = (settingsButton == &m_generalSettings.ccSelectProvider)
? 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:"));
});
}
void ConfigurationManager::selectModel()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
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)) {
if (!provider->capabilities().testFlag(PluginLLMCore::ProviderCapability::ModelListing)) {
m_generalSettings.showModelsNotSupportedDialog(*targetSettings);
return;
}
provider->getInstalledModels(providerUrl)
.then(this, [this, targetSettings](const QList<QString> &modelList) {
if (modelList.isEmpty()) {
m_generalSettings.showModelsNotFoundDialog(*targetSettings);
return;
}
m_generalSettings.showSelectionDialog(
modelList, *targetSettings, Tr::tr("Select LLM Model"), Tr::tr("Models:"));
});
}
}
void ConfigurationManager::selectTemplate()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
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.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:"));
});
}
void ConfigurationManager::selectUrl()
{
auto *settingsButton = qobject_cast<ButtonAspect *>(sender());
if (!settingsButton)
return;
QStringList urls;
for (const auto &name : m_providersManager.providersNames()) {
const auto url = m_providersManager.getProviderByName(name)->url();
if (!urls.contains(url))
urls.append(url);
}
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]() {
m_generalSettings.showUrlSelectionDialog(targetSettings, urls);
});
}
} // namespace QodeAssist

47
ConfigurationManager.hpp Normal file
View File

@@ -0,0 +1,47 @@
// Copyright (C) 2025-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QObject>
#include "pluginllmcore/PromptTemplateManager.hpp"
#include "pluginllmcore/ProvidersManager.hpp"
#include "settings/GeneralSettings.hpp"
namespace QodeAssist {
class ConfigurationManager : public QObject
{
Q_OBJECT
public:
static ConfigurationManager &instance();
void init();
void updateTemplateDescription(const Utils::StringAspect &templateAspect);
void updateAllTemplateDescriptions();
void checkTemplate(const Utils::StringAspect &templateAspect);
void checkAllTemplate();
public slots:
void selectProvider();
void selectModel();
void selectTemplate();
void selectUrl();
private:
explicit ConfigurationManager(QObject *parent = nullptr);
~ConfigurationManager() = default;
ConfigurationManager(const ConfigurationManager &) = delete;
ConfigurationManager &operator=(const ConfigurationManager &) = delete;
Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::ProvidersManager &m_providersManager;
PluginLLMCore::PromptTemplateManager &m_templateManger;
void setupConnections();
};
} // namespace QodeAssist

View File

@@ -1,212 +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/>.
*/
#include "DocumentContextReader.hpp"
#include <QFileInfo>
#include <QTextBlock>
#include <languageserverprotocol/lsptypes.h>
#include "settings/ContextSettings.hpp"
const QRegularExpression &getYearRegex()
{
static const QRegularExpression yearRegex("\\b(19|20)\\d{2}\\b");
return yearRegex;
}
const QRegularExpression &getNameRegex()
{
static const QRegularExpression nameRegex("\\b[A-Z][a-z.]+ [A-Z][a-z.]+\\b");
return nameRegex;
}
const QRegularExpression &getCommentRegex()
{
static const QRegularExpression
commentRegex(R"((/\*[\s\S]*?\*/|//.*$|#.*$|//{2,}[\s\S]*?//{2,}))",
QRegularExpression::MultilineOption);
return commentRegex;
}
namespace QodeAssist {
DocumentContextReader::DocumentContextReader(TextEditor::TextDocument *textDocument)
: m_textDocument(textDocument)
, m_document(textDocument->document())
{
m_copyrightInfo = findCopyright();
}
QString DocumentContextReader::getLineText(int lineNumber, int cursorPosition) const
{
if (!m_document || lineNumber < 0)
return QString();
QTextBlock block = m_document->begin();
int currentLine = 0;
while (block.isValid()) {
if (currentLine == lineNumber) {
QString text = block.text();
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
text = text.left(cursorPosition);
}
return text;
}
block = block.next();
currentLine++;
}
return QString();
}
QString DocumentContextReader::getContextBefore(int lineNumber,
int cursorPosition,
int linesCount) const
{
int effectiveStartLine;
if (m_copyrightInfo.found) {
effectiveStartLine = qMax(m_copyrightInfo.endLine + 1, lineNumber - linesCount);
} else {
effectiveStartLine = qMax(0, lineNumber - linesCount);
}
return getContextBetween(effectiveStartLine, lineNumber, cursorPosition);
}
QString DocumentContextReader::getContextAfter(int lineNumber,
int cursorPosition,
int linesCount) const
{
int endLine = qMin(m_document->blockCount() - 1, lineNumber + linesCount);
return getContextBetween(lineNumber + 1, endLine, cursorPosition);
}
QString DocumentContextReader::readWholeFileBefore(int lineNumber, int cursorPosition) const
{
int startLine = 0;
if (m_copyrightInfo.found) {
startLine = m_copyrightInfo.endLine + 1;
}
startLine = qMin(startLine, lineNumber);
QString result = getContextBetween(startLine, lineNumber, cursorPosition);
return result;
}
QString DocumentContextReader::readWholeFileAfter(int lineNumber, int cursorPosition) const
{
return getContextBetween(lineNumber, m_document->blockCount() - 1, cursorPosition);
}
QString DocumentContextReader::getLanguageAndFileInfo() const
{
if (!m_textDocument)
return QString();
QString language = LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(
m_textDocument->mimeType());
QString mimeType = m_textDocument->mimeType();
QString filePath = m_textDocument->filePath().toString();
QString fileExtension = QFileInfo(filePath).suffix();
return QString("Language: %1 (MIME: %2) filepath: %3(%4)\n\n")
.arg(language, mimeType, filePath, fileExtension);
}
QString DocumentContextReader::getSpecificInstructions() const
{
QString specificInstruction = Settings::contextSettings().specificInstractions().arg(
LanguageServerProtocol::TextDocumentItem::mimeTypeToLanguageId(m_textDocument->mimeType()));
return QString("%1").arg(specificInstruction);
}
CopyrightInfo DocumentContextReader::findCopyright()
{
CopyrightInfo result = {-1, -1, false};
QString text = m_document->toPlainText();
QRegularExpressionMatchIterator matchIterator = getCommentRegex().globalMatch(text);
QList<CopyrightInfo> copyrightBlocks;
while (matchIterator.hasNext()) {
QRegularExpressionMatch match = matchIterator.next();
QString matchedText = match.captured().toLower();
if (matchedText.contains("copyright") || matchedText.contains("(C)")
|| matchedText.contains("(c)") || matchedText.contains("©")
|| getYearRegex().match(text).hasMatch() || getNameRegex().match(text).hasMatch()) {
int startPos = match.capturedStart();
int endPos = match.capturedEnd();
CopyrightInfo info;
info.startLine = m_document->findBlock(startPos).blockNumber();
info.endLine = m_document->findBlock(endPos).blockNumber();
info.found = true;
copyrightBlocks.append(info);
}
}
for (int i = 0; i < copyrightBlocks.size() - 1; ++i) {
if (copyrightBlocks[i].endLine + 1 >= copyrightBlocks[i + 1].startLine) {
copyrightBlocks[i].endLine = copyrightBlocks[i + 1].endLine;
copyrightBlocks.removeAt(i + 1);
--i;
}
}
if (!copyrightBlocks.isEmpty()) { // temproary solution, need cache
return copyrightBlocks.first();
}
return result;
}
QString DocumentContextReader::getContextBetween(int startLine,
int endLine,
int cursorPosition) const
{
QString context;
for (int i = startLine; i <= endLine; ++i) {
QTextBlock block = m_document->findBlockByNumber(i);
if (!block.isValid()) {
break;
}
if (i == endLine) {
context += block.text().left(cursorPosition);
} else {
context += block.text() + "\n";
}
}
return context;
}
CopyrightInfo DocumentContextReader::copyrightInfo() const
{
return m_copyrightInfo;
}
} // namespace QodeAssist

View File

@@ -1,57 +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/>.
*/
#pragma once
#include <QTextDocument>
#include <texteditor/textdocument.h>
namespace QodeAssist {
struct CopyrightInfo
{
int startLine;
int endLine;
bool found;
};
class DocumentContextReader
{
public:
DocumentContextReader(TextEditor::TextDocument *textDocument);
QString getLineText(int lineNumber, int cursorPosition = -1) const;
QString getContextBefore(int lineNumber, int cursorPosition, int linesCount) const;
QString getContextAfter(int lineNumber, int cursorPosition, int linesCount) const;
QString readWholeFileBefore(int lineNumber, int cursorPosition) const;
QString readWholeFileAfter(int lineNumber, int cursorPosition) const;
QString getLanguageAndFileInfo() const;
QString getSpecificInstructions() const;
CopyrightInfo findCopyright();
QString getContextBetween(int startLine, int endLine, int cursorPosition) const;
CopyrightInfo copyrightInfo() const;
private:
TextEditor::TextDocument *m_textDocument;
QTextDocument *m_document;
CopyrightInfo m_copyrightInfo;
};
} // namespace QodeAssist

View File

@@ -1,49 +1,48 @@
/*
* 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/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "LLMClientInterface.hpp"
#include <LLMQore/BaseClient.hpp>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <texteditor/textdocument.h>
#include "DocumentContextReader.hpp"
#include "LLMProvidersManager.hpp"
#include "PromptTemplateManager.hpp"
#include "QodeAssistUtils.hpp"
#include "core/ChangesManager.h"
#include "settings/ContextSettings.hpp"
#include "CodeHandler.hpp"
#include "context/DocumentContextReader.hpp"
#include "context/Utils.hpp"
#include "logger/Logger.hpp"
#include "settings/CodeCompletionSettings.hpp"
#include "settings/GeneralSettings.hpp"
#include <pluginllmcore/RulesLoader.hpp>
namespace QodeAssist {
LLMClientInterface::LLMClientInterface()
: m_manager(new QNetworkAccessManager(this))
LLMClientInterface::LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::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))
{
updateProvider();
}
LLMClientInterface::~LLMClientInterface()
{
handleCancelRequest();
}
Utils::FilePath LLMClientInterface::serverDeviceTemplate() const
{
return "Qode Assist";
return "QodeAssist";
}
void LLMClientInterface::startImpl()
@@ -51,10 +50,47 @@ 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;
const RequestContext &ctx = it.value();
LOG_MESSAGE(QString("Request %1 failed: %2").arg(requestId, error));
// Send LSP error response to client
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = ctx.originalRequest["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = error;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
m_activeRequests.erase(it);
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::sendData(const QByteArray &data)
{
updateProvider();
QJsonDocument doc = QJsonDocument::fromJson(data);
if (!doc.isObject())
return;
@@ -71,100 +107,39 @@ void LLMClientInterface::sendData(const QByteArray &data)
} else if (method == "textDocument/didOpen") {
handleTextDocumentDidOpen(request);
} else if (method == "getCompletionsCycling") {
QString requestId = request["id"].toString();
startTimeMeasurement(requestId);
handleCompletion(request);
} else if (method == "$/cancelRequest") {
handleCancelRequest(request);
handleCancelRequest();
} else if (method == "exit") {
// TODO make exit handler
} else {
logMessage(QString("Unknown method: %1").arg(method));
LOG_MESSAGE(QString("Unknown method: %1").arg(method));
}
}
void LLMClientInterface::handleCancelRequest(const QJsonObject &request)
void LLMClientInterface::handleCancelRequest()
{
QString id = request["params"].toObject()["id"].toString();
if (m_activeRequests.contains(id)) {
m_activeRequests[id]->abort();
m_activeRequests.remove(id);
logMessage(QString("Request %1 cancelled successfully").arg(id));
} else {
logMessage(QString("Request %1 not found").arg(id));
}
}
bool LLMClientInterface::processSingleLineCompletion(QNetworkReply *reply,
const QJsonObject &request,
const QString &accumulatedCompletion)
{
int newlinePos = accumulatedCompletion.indexOf('\n');
if (newlinePos != -1) {
QString singleLineCompletion = accumulatedCompletion.left(newlinePos).trimmed();
singleLineCompletion = removeStopWords(singleLineCompletion);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
sendCompletionToClient(singleLineCompletion, request, position, true);
m_accumulatedResponses.remove(reply);
reply->abort();
return true;
}
return false;
}
QString LLMClientInterface::сontextBefore(TextEditor::TextEditorWidget *widget,
int lineNumber,
int cursorPosition)
{
if (!widget)
return QString();
DocumentContextReader reader(widget->textDocument());
const auto &copyright = reader.copyrightInfo();
logMessage(QString{"Line Number: %1"}.arg(lineNumber));
logMessage(QString("Copyright found %1 %2").arg(copyright.found).arg(copyright.endLine));
if (lineNumber < reader.findCopyright().endLine)
return QString();
QString contextBefore;
if (Settings::contextSettings().readFullFile()) {
contextBefore = reader.readWholeFileBefore(lineNumber, cursorPosition);
} else {
contextBefore
= reader.getContextBefore(lineNumber,
cursorPosition,
Settings::contextSettings().readStringsBeforeCursor());
QSet<PluginLLMCore::Provider *> providers;
for (auto it = m_activeRequests.begin(); it != m_activeRequests.end(); ++it) {
if (it.value().provider) {
providers.insert(it.value().provider);
}
}
return contextBefore;
}
QString LLMClientInterface::сontextAfter(TextEditor::TextEditorWidget *widget,
int lineNumber,
int cursorPosition)
{
if (!widget)
return QString();
DocumentContextReader reader(widget->textDocument());
if (lineNumber < reader.findCopyright().endLine)
return QString();
QString contextAfter;
if (Settings::contextSettings().readFullFile()) {
contextAfter = reader.readWholeFileAfter(lineNumber, cursorPosition);
} else {
contextAfter = reader.getContextAfter(lineNumber,
cursorPosition,
Settings::contextSettings().readStringsAfterCursor());
for (auto *provider : providers) {
disconnect(provider->client(), nullptr, this, nullptr);
}
return contextAfter;
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)
@@ -217,107 +192,233 @@ void LLMClientInterface::handleExit(const QJsonObject &request)
emit finished();
}
void LLMClientInterface::handleLLMResponse(QNetworkReply *reply, const QJsonObject &request)
{
QString &accumulatedResponse = m_accumulatedResponses[reply];
auto &templateManager = PromptTemplateManager::instance();
const Templates::PromptTemplate *currentTemplate = templateManager.getCurrentTemplate();
auto &providerManager = LLMProvidersManager::instance();
bool isComplete = providerManager.getCurrentProvider()->handleResponse(reply,
accumulatedResponse);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
if (!Settings::generalSettings().multiLineCompletion()
&& processSingleLineCompletion(reply, request, accumulatedResponse)) {
return;
}
if (isComplete || reply->isFinished()) {
if (isComplete) {
auto cleanedCompletion = removeStopWords(accumulatedResponse);
sendCompletionToClient(cleanedCompletion, request, position, true);
} else {
handleCompletion(request, accumulatedResponse);
}
m_accumulatedResponses.remove(reply);
}
}
void LLMClientInterface::handleCompletion(const QJsonObject &request,
const QStringView &accumulatedCompletion)
{
auto updatedContext = prepareContext(request, accumulatedCompletion);
sendLLMRequest(request, updatedContext);
}
ContextData LLMClientInterface::prepareContext(const QJsonObject &request,
const QStringView &accumulatedCompletion)
{
QJsonObject params = request["params"].toObject();
QJsonObject doc = params["doc"].toObject();
QJsonObject position = doc["position"].toObject();
QString uri = doc["uri"].toString();
Utils::FilePath filePath = Utils::FilePath::fromString(QUrl(uri).toLocalFile());
TextEditor::TextDocument *textDocument = TextEditor::TextDocument::textDocumentForFilePath(
filePath);
if (!textDocument) {
logMessage("Error: Document is not available for" + filePath.toString());
return ContextData{};
}
int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt();
auto textEditor = TextEditor::BaseTextEditor::currentTextEditor();
TextEditor::TextEditorWidget *widget = textEditor->editorWidget();
DocumentContextReader reader(widget->textDocument());
QString recentChanges = ChangesManager::instance().getRecentChangesContext(textDocument);
QString contextBefore = сontextBefore(widget, lineNumber, cursorPosition);
QString contextAfter = сontextAfter(widget, lineNumber, cursorPosition);
QString instructions
= QString("%1%2%3").arg(Settings::contextSettings().useSpecificInstructions()
? reader.getSpecificInstructions()
: QString(),
Settings::contextSettings().useFilePathInContext()
? reader.getLanguageAndFileInfo()
: QString(),
Settings::contextSettings().useProjectChangesCache() ? recentChanges
: QString());
return {QString("%1%2").arg(contextBefore, accumulatedCompletion), contextAfter, instructions};
}
void LLMClientInterface::updateProvider()
{
m_serverUrl = QUrl(QString("%1%2").arg(Settings::generalSettings().url(),
Settings::generalSettings().endPoint()));
}
void LLMClientInterface::sendCompletionToClient(const QString &completion,
const QJsonObject &request,
const QJsonObject &position,
bool isComplete)
void LLMClientInterface::sendErrorResponse(const QJsonObject &request, const QString &errorMessage)
{
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject errorObject;
errorObject["code"] = -32603; // Internal error code
errorObject["message"] = errorMessage;
response["error"] = errorObject;
emit messageReceived(LanguageServerProtocol::JsonRpcMessage(response));
// End performance measurement if it was started
QString requestId = request["id"].toString();
m_performanceLogger.endTimeMeasurement(requestId);
}
void LLMClientInterface::handleCompletion(const QJsonObject &request)
{
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 updatedContext = prepareContext(request, documentInfo);
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) {
QString error = QString("No template found with name: %1").arg(templateName);
LOG_MESSAGE(error);
sendErrorResponse(request, error);
return;
}
QJsonObject payload{{"model", modelName}, {"stream", true}};
const auto stopWords = QJsonArray::fromStringList(promptTemplate->stopWords());
if (!stopWords.isEmpty())
payload["stop"] = stopWords;
QString systemPrompt;
if (m_completeSettings.useSystemPrompt())
systemPrompt.append(
m_completeSettings.useUserMessageTemplateForCC()
&& promptTemplate->type() == PluginLLMCore::TemplateType::Chat
? m_completeSettings.systemPromptForNonFimModels()
: m_completeSettings.systemPrompt());
auto project = PluginLLMCore::RulesLoader::getActiveProject();
if (project) {
QString projectRules
= PluginLLMCore::RulesLoader::loadRulesForProject(project, PluginLLMCore::RulesContext::Completions);
if (!projectRules.isEmpty()) {
systemPrompt += "\n\n# Project Rules\n\n" + projectRules;
LOG_MESSAGE("Loaded project rules for completion");
}
}
if (updatedContext.fileContext.has_value())
systemPrompt.append(updatedContext.fileContext.value());
if (m_completeSettings.useOpenFilesContext()) {
if (provider->providerID() == PluginLLMCore::ProviderID::LlamaCpp) {
for (const auto openedFilePath : m_contextManager->openedFiles({filePath})) {
if (!updatedContext.filesMetadata) {
updatedContext.filesMetadata = QList<PluginLLMCore::FileMetadata>();
}
updatedContext.filesMetadata->append({openedFilePath.first, openedFilePath.second});
}
} else {
systemPrompt.append(m_contextManager->openedFilesContext({filePath}));
}
}
updatedContext.systemPrompt = systemPrompt;
if (promptTemplate->type() == PluginLLMCore::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<PluginLLMCore::Message> messages;
messages.append({"user", userMessage});
updatedContext.history = messages;
}
provider->prepareRequest(
payload,
promptTemplate,
updatedContext,
PluginLLMCore::RequestType::CodeCompletion,
false,
false);
connect(
provider->client(),
&::LLMQore::BaseClient::requestCompleted,
this,
&LLMClientInterface::handleFullResponse,
Qt::UniqueConnection);
connect(
provider->client(),
&::LLMQore::BaseClient::requestFailed,
this,
&LLMClientInterface::handleRequestFailed,
Qt::UniqueConnection);
auto requestId
= provider->sendRequest(QUrl(url), payload, resolveEndpoint(promptTemplate, isPreset1Active));
m_activeRequests[requestId] = {request, provider};
m_performanceLogger.startTimeMeasurement(requestId);
}
PluginLLMCore::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();
int cursorPosition = position["character"].toInt();
int lineNumber = position["line"].toInt();
Context::DocumentContextReader
reader(documentInfo.document, documentInfo.mimeType, documentInfo.filePath);
return reader.prepareContext(lineNumber, cursorPosition, m_completeSettings);
}
QString LLMClientInterface::resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const
{
const QString custom = isLanguageSpecify ? m_generalSettings.ccPreset1CustomEndpoint()
: m_generalSettings.ccCustomEndpoint();
return !custom.isEmpty() ? custom : promptTemplate->endpoint();
}
Context::ContextManager *LLMClientInterface::contextManager() const
{
return m_contextManager;
}
void LLMClientInterface::sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete)
{
auto filePath = Context::extractFilePathFromRequest(request);
auto documentInfo = m_documentReader.readDocument(filePath);
bool isPreset1Active = m_contextManager->isSpecifyCompletion(documentInfo);
auto templateName = !isPreset1Active ? m_generalSettings.ccTemplate()
: m_generalSettings.ccPreset1Template();
auto promptTemplate = m_promptProvider->getTemplateByName(templateName);
QJsonObject position = request["params"].toObject()["doc"].toObject()["position"].toObject();
QJsonObject response;
response["jsonrpc"] = "2.0";
response[LanguageServerProtocol::idKey] = request["id"];
QJsonObject result;
QJsonArray completions;
QJsonObject completionItem;
completionItem[LanguageServerProtocol::textKey] = 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() + completion.length();
range["end"] = end;
range["end"] = position;
completionItem[LanguageServerProtocol::rangeKey] = range;
completionItem[LanguageServerProtocol::positionKey] = position;
completions.append(completionItem);
@@ -325,104 +426,17 @@ void LLMClientInterface::sendCompletionToClient(const QString &completion,
result[LanguageServerProtocol::isIncompleteKey] = !isComplete;
response[LanguageServerProtocol::resultKey] = result;
logMessage(
LOG_MESSAGE(
QString("Completions: \n%1")
.arg(QString::fromUtf8(QJsonDocument(completions).toJson(QJsonDocument::Indented))));
logMessage(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::sendLLMRequest(const QJsonObject &request, const ContextData &prompt)
{
QJsonObject providerRequest = {{"model", Settings::generalSettings().modelName.value()},
{"stream", true}};
auto currentTemplate = PromptTemplateManager::instance().getCurrentTemplate();
currentTemplate->prepareRequest(providerRequest, prompt);
auto &providerManager = LLMProvidersManager::instance();
providerManager.getCurrentProvider()->prepareRequest(providerRequest);
logMessage(QString("Sending request to llm: \nurl: %1\nRequest body:\n%2")
.arg(m_serverUrl.toString(),
QString::fromUtf8(
QJsonDocument(providerRequest).toJson(QJsonDocument::Indented))));
QNetworkRequest networkRequest(m_serverUrl);
networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
if (providerRequest.contains("api_key")) {
QString apiKey = providerRequest["api_key"].toString();
networkRequest.setRawHeader("Authorization", QString("Bearer %1").arg(apiKey).toUtf8());
providerRequest.remove("api_key");
}
QNetworkReply *reply = m_manager->post(networkRequest, QJsonDocument(providerRequest).toJson());
if (!reply) {
logMessage("Error: Failed to create network reply");
return;
}
QString requestId = request["id"].toString();
m_activeRequests[requestId] = reply;
connect(reply, &QNetworkReply::readyRead, this, [this, reply, request]() {
handleLLMResponse(reply, request);
});
connect(reply, &QNetworkReply::finished, this, [this, reply, requestId]() {
reply->deleteLater();
m_activeRequests.remove(requestId);
if (reply->error() != QNetworkReply::NoError) {
logMessage(QString("Error in QodeAssist request: %1").arg(reply->errorString()));
} else {
logMessage("Request finished successfully");
}
});
}
QString LLMClientInterface::removeStopWords(const QStringView &completion)
{
QString filteredCompletion = completion.toString();
auto currentTemplate = PromptTemplateManager::instance().getCurrentTemplate();
QStringList stopWords = currentTemplate->stopWords();
for (const QString &stopWord : stopWords) {
filteredCompletion = filteredCompletion.replace(stopWord, "");
}
return filteredCompletion;
}
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)
{
logMessage(QString("Performance: %1 %2 took %3 ms").arg(requestId, operation).arg(elapsedMs));
}
void LLMClientInterface::parseCurrentMessage() {}
} // namespace QodeAssist

View File

@@ -1,28 +1,20 @@
/*
* 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/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <languageclient/languageclientinterface.h>
#include <texteditor/texteditor.h>
#include "QodeAssistData.hpp"
#include <context/ContextManager.hpp>
#include <context/IDocumentReader.hpp>
#include <context/ProgrammingLanguage.hpp>
#include <pluginllmcore/ContextData.hpp>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/IProviderRegistry.hpp>
#include <logger/IRequestPerformanceLogger.hpp>
#include <settings/CodeCompletionSettings.hpp>
#include <settings/GeneralSettings.hpp>
class QNetworkReply;
class QNetworkAccessManager;
@@ -34,29 +26,33 @@ class LLMClientInterface : public LanguageClient::BaseClientInterface
Q_OBJECT
public:
LLMClientInterface();
LLMClientInterface(
const Settings::GeneralSettings &generalSettings,
const Settings::CodeCompletionSettings &completeSettings,
PluginLLMCore::IProviderRegistry &providerRegistry,
PluginLLMCore::IPromptProvider *promptProvider,
Context::IDocumentReader &documentReader,
IRequestPerformanceLogger &performanceLogger);
~LLMClientInterface() override;
public:
Utils::FilePath serverDeviceTemplate() const override;
void sendCompletionToClient(const QString &completion,
const QJsonObject &request,
const QJsonObject &position,
bool isComplete);
void sendCompletionToClient(
const QString &completion, const QJsonObject &request, bool isComplete);
void handleCompletion(const QJsonObject &request,
const QStringView &accumulatedCompletion = QString());
void sendLLMRequest(const QJsonObject &request, const ContextData &prompt);
void handleLLMResponse(QNetworkReply *reply, const QJsonObject &request);
void handleCompletion(const QJsonObject &request);
ContextData prepareContext(const QJsonObject &request,
const QStringView &accumulatedCompletion = QString{});
void updateProvider();
// 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);
@@ -64,26 +60,30 @@ private:
void handleTextDocumentDidOpen(const QJsonObject &request);
void handleInitialized(const QJsonObject &request);
void handleExit(const QJsonObject &request);
void handleCancelRequest(const QJsonObject &request);
bool processSingleLineCompletion(QNetworkReply *reply,
const QJsonObject &request,
const QString &accumulatedCompletion);
void handleCancelRequest();
void sendErrorResponse(const QJsonObject &request, const QString &errorMessage);
QString сontextBefore(TextEditor::TextEditorWidget *widget, int lineNumber, int cursorPosition);
QString сontextAfter(TextEditor::TextEditorWidget *widget, int lineNumber, int cursorPosition);
QString removeStopWords(const QStringView &completion);
struct RequestContext
{
QJsonObject originalRequest;
PluginLLMCore::Provider *provider;
};
QUrl m_serverUrl;
QNetworkAccessManager *m_manager;
QMap<QString, QNetworkReply *> m_activeRequests;
QMap<QNetworkReply *, QString> m_accumulatedResponses;
PluginLLMCore::ContextData prepareContext(
const QJsonObject &request, const Context::DocumentInfo &documentInfo);
QString resolveEndpoint(
PluginLLMCore::PromptTemplate *promptTemplate, bool isLanguageSpecify) const;
const Settings::CodeCompletionSettings &m_completeSettings;
const Settings::GeneralSettings &m_generalSettings;
PluginLLMCore::IPromptProvider *m_promptProvider = nullptr;
PluginLLMCore::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,56 +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/>.
*/
#include "LLMProvidersManager.hpp"
namespace QodeAssist {
LLMProvidersManager &LLMProvidersManager::instance()
{
static LLMProvidersManager instance;
return instance;
}
QStringList LLMProvidersManager::getProviderNames() const
{
return m_providers.keys();
}
void LLMProvidersManager::setCurrentProvider(const QString &name)
{
if (m_providers.contains(name)) {
m_currentProviderName = name;
}
}
Providers::LLMProvider *LLMProvidersManager::getCurrentProvider()
{
if (m_currentProviderName.isEmpty()) {
return nullptr;
}
return m_providers[m_currentProviderName];
}
LLMProvidersManager::~LLMProvidersManager()
{
qDeleteAll(m_providers);
}
} // namespace QodeAssist

View File

@@ -1,58 +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/>.
*/
#pragma once
#include <QString>
#include "providers/LLMProvider.hpp"
namespace QodeAssist {
class LLMProvidersManager
{
public:
static LLMProvidersManager &instance();
template<typename T>
void registerProvider()
{
static_assert(std::is_base_of<Providers::LLMProvider, T>::value,
"T must inherit from LLMProvider");
T *provider = new T();
QString name = provider->name();
m_providers[name] = provider;
}
QStringList getProviderNames() const;
void setCurrentProvider(const QString &name);
Providers::LLMProvider *getCurrentProvider();
~LLMProvidersManager();
private:
LLMProvidersManager() = default;
LLMProvidersManager(const LLMProvidersManager &) = delete;
LLMProvidersManager &operator=(const LLMProvidersManager &) = delete;
QMap<QString, Providers::LLMProvider *> m_providers;
QString m_currentProviderName;
};
} // namespace QodeAssist

View File

@@ -1,124 +1,222 @@
/*
* 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/>.
*/
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#include "LLMSuggestion.hpp"
#include <QTextCursor>
#include <QtWidgets/qtoolbar.h>
#include <texteditor/texteditor.h>
#include <utils/stringutils.h>
#include <utils/tooltip/tooltip.h>
namespace QodeAssist {
LLMSuggestion::LLMSuggestion(const Completion &completion, QTextDocument *origin)
: m_completion(completion)
, m_linesCount(0)
static bool isClosingTail(const QString &s, int from)
{
int startPos = completion.range().start().toPositionInDocument(origin);
int endPos = completion.range().end().toPositionInDocument(origin);
static const QString closeChars = QStringLiteral("(){}[];,");
for (int i = from; i < s.size(); ++i) {
const QChar c = s.at(i);
if (!c.isSpace() && !closeChars.contains(c))
return false;
}
return true;
}
startPos = qBound(0, startPos, origin->characterCount() - 1);
endPos = qBound(startPos, endPos, origin->characterCount() - 1);
int LLMSuggestion::calculateReplaceLength(const QString &suggestion, const QString &rightText)
{
if (rightText.isEmpty())
return 0;
m_start = QTextCursor(origin);
m_start.setPosition(startPos);
m_start.setKeepPositionOnInsert(true);
const int maxN = qMin(suggestion.size(), rightText.size());
int lcp = 0;
while (lcp < maxN && suggestion.at(lcp) == rightText.at(lcp))
++lcp;
QTextCursor cursor(origin);
if (lcp > 0) {
if (isClosingTail(rightText, lcp))
return rightText.size();
return lcp;
}
if (!isClosingTail(rightText, 0))
return 0;
static const QString closeChars = QStringLiteral("(){}[];,");
int i = suggestion.size() - 1;
while (i >= 0 && suggestion.at(i).isSpace())
--i;
if (i >= 0 && closeChars.contains(suggestion.at(i)) && rightText.contains(suggestion.at(i)))
return rightText.size();
return 0;
}
LLMSuggestion::LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion)
: TextEditor::CyclicSuggestion(suggestions, sourceDocument, currentCompletion)
{
const auto &data = suggestions[currentCompletion];
int startPos = data.range.begin.toPositionInDocument(sourceDocument);
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, completion.text());
QString suggestionText = data.text;
document()->setPlainText(blockText);
if (!suggestionText.contains('\n')) {
int replaceLength = calculateReplaceLength(suggestionText, rightText);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
setCurrentPosition(m_start.position());
}
QString displayText = leftText + suggestionText + remainingRightText;
replacementDocument()->setPlainText(displayText);
} else {
int firstLineEnd = suggestionText.indexOf('\n');
QString firstLine = suggestionText.left(firstLineEnd);
QString restOfCompletion = suggestionText.mid(firstLineEnd);
bool LLMSuggestion::apply()
{
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
cursor.insertText(m_completion.text());
cursor.endEditBlock();
return true;
int replaceLength = calculateReplaceLength(firstLine, rightText);
QString remainingRightText = (replaceLength > 0) ? rightText.mid(replaceLength) : rightText;
QString displayText = leftText + firstLine + remainingRightText + restOfCompletion;
replacementDocument()->setPlainText(displayText);
}
}
bool LLMSuggestion::applyWord(TextEditor::TextEditorWidget *widget)
{
return applyNextLine(widget);
return applyPart(Word, widget);
}
bool LLMSuggestion::applyNextLine(TextEditor::TextEditorWidget *widget)
bool LLMSuggestion::applyLine(TextEditor::TextEditorWidget *widget)
{
const QString text = m_completion.text();
QStringList lines = text.split('\n');
if (m_linesCount < lines.size())
m_linesCount++;
showTooltip(widget, m_linesCount);
return m_linesCount == lines.size() && !Utils::ToolTip::isVisible();
return applyPart(Line, widget);
}
void LLMSuggestion::onCounterFinished(int count)
bool LLMSuggestion::applyPart(Part part, TextEditor::TextEditorWidget *widget)
{
Utils::ToolTip::hide();
m_linesCount = 0;
QTextCursor cursor = m_completion.range().toSelection(m_start.document());
cursor.beginEditBlock();
cursor.removeSelectedText();
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 = currentData.text;
QStringList lines = m_completion.text().split('\n');
QString textToInsert = lines.mid(0, count).join('\n');
const int startPos = currentCursor.positionInBlock() - cursor.positionInBlock()
+ (cursor.selectionEnd() - cursor.selectionStart());
cursor.insertText(textToInsert);
cursor.endEditBlock();
int next = part == Word ? Utils::endOfNextWord(text, startPos) : text.indexOf('\n', startPos);
if (next == -1) {
if (part == Line) {
next = text.length();
} else {
return apply();
}
}
if (part == Line)
++next;
QString subText = text.mid(startPos, next - startPos);
if (subText.isEmpty()) {
return false;
}
if (startPos == 0) {
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
int replaceLength = calculateReplaceLength(text, textAfterCursor);
if (replaceLength > 0) {
currentCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
currentCursor.removeSelectedText();
}
}
if (!subText.contains('\n')) {
currentCursor.insertText(subText);
const QString remainingText = text.mid(next);
if (!remainingText.isEmpty()) {
QTextCursor newCursor = widget->textCursor();
const Utils::Text::Position newStart = Utils::Text::Position::fromPositionInDocument(
newCursor.document(), newCursor.position());
const Utils::Text::Position
newEnd{newStart.line, newStart.column + int(remainingText.length())};
const Utils::Text::Range newRange{newStart, newEnd};
const QList<Data> newSuggestion{{newRange, newStart, remainingText}};
widget->insertSuggestion(
std::make_unique<LLMSuggestion>(newSuggestion, widget->document(), 0));
}
} else {
currentCursor.insertText(subText);
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;
}
void LLMSuggestion::reset()
bool LLMSuggestion::apply()
{
m_start.removeSelectedText();
}
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;
int LLMSuggestion::position()
{
return m_start.position();
}
QTextBlock currentBlock = cursor.block();
QString textAfterCursor = currentBlock.text().mid(cursor.positionInBlock());
void LLMSuggestion::showTooltip(TextEditor::TextEditorWidget *widget, int count)
{
Utils::ToolTip::hide();
QPoint pos = widget->mapToGlobal(widget->cursorRect().topRight());
pos += QPoint(-10, -50);
m_counterTooltip = new CounterTooltip(count);
Utils::ToolTip::show(pos, m_counterTooltip, widget);
connect(m_counterTooltip, &CounterTooltip::finished, this, &LLMSuggestion::onCounterFinished);
QTextCursor editCursor = cursor;
editCursor.beginEditBlock();
int firstLineEnd = text.indexOf('\n');
if (firstLineEnd != -1) {
QString firstLine = text.left(firstLineEnd);
QString restOfText = text.mid(firstLineEnd);
int replaceLength = calculateReplaceLength(firstLine, textAfterCursor);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(firstLine + restOfText);
} else {
int replaceLength = calculateReplaceLength(text, textAfterCursor);
if (replaceLength > 0) {
editCursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, replaceLength);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
}
editCursor.endEditBlock();
return true;
}
} // namespace QodeAssist

View File

@@ -1,8 +1,13 @@
/*
* Copyright (C) 2024 Petr Mironychev
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024-2026 Petr Mironychev
*
* This file is part of QodeAssist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
@@ -19,37 +24,24 @@
#pragma once
#include <QObject>
#include "LSPCompletion.hpp"
#include <texteditor/textdocumentlayout.h>
#include "utils/CounterTooltip.hpp"
#include <texteditor/texteditor.h>
#include <texteditor/textsuggestion.h>
namespace QodeAssist {
class LLMSuggestion final : public QObject, public TextEditor::TextSuggestion
class LLMSuggestion : public TextEditor::CyclicSuggestion
{
Q_OBJECT
public:
LLMSuggestion(const Completion &completion, QTextDocument *origin);
enum Part { Word, Line };
bool apply() final;
bool applyWord(TextEditor::TextEditorWidget *widget) final;
bool applyNextLine(TextEditor::TextEditorWidget *widget);
void reset() final;
int position() final;
LLMSuggestion(
const QList<Data> &suggestions, QTextDocument *sourceDocument, int currentCompletion = 0);
const Completion &completion() const { return m_completion; }
bool applyWord(TextEditor::TextEditorWidget *widget) override;
bool applyLine(TextEditor::TextEditorWidget *widget) override;
bool applyPart(Part part, TextEditor::TextEditorWidget *widget);
bool apply() override;
void showTooltip(TextEditor::TextEditorWidget *widget, int count);
void onCounterFinished(int count);
private:
Completion m_completion;
QTextCursor m_start;
int m_linesCount;
CounterTooltip *m_counterTooltip = nullptr;
static int calculateReplaceLength(const QString &suggestion, const QString &rightText);
};
} // namespace QodeAssist

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
* Copyright (C) 2024-2026 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,53 +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/>.
*/
#include "PromptTemplateManager.hpp"
namespace QodeAssist {
PromptTemplateManager &PromptTemplateManager::instance()
{
static PromptTemplateManager instance;
return instance;
}
void PromptTemplateManager::setCurrentTemplate(const QString &name)
{
if (m_templates.contains(name)) {
m_currentTemplateName = name;
}
}
const Templates::PromptTemplate *PromptTemplateManager::getCurrentTemplate() const
{
auto it = m_templates.find(m_currentTemplateName);
return it != m_templates.end() ? it.value() : nullptr;
}
QStringList PromptTemplateManager::getTemplateNames() const
{
return m_templates.keys();
}
PromptTemplateManager::~PromptTemplateManager()
{
qDeleteAll(m_templates);
}
} // namespace QodeAssist

View File

@@ -1,59 +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/>.
*/
#pragma once
#include <QMap>
#include <QString>
#include "templates/PromptTemplate.hpp"
namespace QodeAssist {
class PromptTemplateManager
{
public:
static PromptTemplateManager &instance();
template<typename T>
void registerTemplate()
{
static_assert(std::is_base_of<Templates::PromptTemplate, T>::value,
"T must inherit from PromptTemplate");
T *template_ptr = new T();
QString name = template_ptr->name();
m_templates[name] = template_ptr;
}
void setCurrentTemplate(const QString &name);
const Templates::PromptTemplate *getCurrentTemplate() const;
QStringList getTemplateNames() const;
~PromptTemplateManager();
private:
PromptTemplateManager() = default;
PromptTemplateManager(const PromptTemplateManager &) = delete;
PromptTemplateManager &operator=(const PromptTemplateManager &) = delete;
QMap<QString, Templates::PromptTemplate *> m_templates;
QString m_currentTemplateName;
};
} // namespace QodeAssist

View File

@@ -1,16 +1,14 @@
{
"Id" : "qodeassist",
"Name" : "QodeAssist",
"Version" : "0.1.2",
"CompatVersion" : "${IDE_VERSION_COMPAT}",
"Version" : "0.9.13",
"CompatVersion" : "${IDE_VERSION}",
"Vendor" : "Petr Mironychev",
"VendorId" : "petrmironychev",
"Copyright" : "(C) ${IDE_COPYRIGHT_YEAR} Petr Mironychev, (C) ${IDE_COPYRIGHT_YEAR} The Qt Company Ltd",
"License" : "GNU General Public License Usage
Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.",
"Description" : ["QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code",
"Prerequisites:",
"- One of the supported LLM providers installed (e.g., Ollama or LM Studio)",
"- A compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2)"],
"License" : "GPLv3",
"Description": "QodeAssist is an AI-powered coding assistant for Qt Creator. It provides intelligent code completion and suggestions for your code. Prerequisites: Requires one of the supported LLM providers installed (e.g., Ollama or LM Studio) and a compatible large language model downloaded for your chosen provider (e.g., CodeLlama, StarCoder2).",
"Url" : "https://github.com/Palm1r/QodeAssist",
"DocumentationUrl" : "https://github.com/Palm1r/QodeAssist",
${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-2026 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,16 +24,27 @@
#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 "core/ChangesManager.h"
#include "settings/ContextSettings.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 "RefactorContextHelper.hpp"
#include <context/ChangesManager.h>
#include <logger/Logger.hpp>
using namespace LanguageServerProtocol;
using namespace TextEditor;
@@ -43,11 +54,96 @@ using namespace Core;
namespace QodeAssist {
QodeAssistClient::QodeAssistClient()
: LanguageClient::Client(new LLMClientInterface())
namespace {
Utils::Text::Position toTextPos(const Utils::Text::Position &pos)
{
return Utils::Text::Position{pos.line, pos.column};
}
bool isIdentifierChar(QChar c)
{
return c.isLetterOrNumber() || c == QLatin1Char('_');
}
bool isInsideIdentifier(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
if (col <= 0 || col > text.size())
return false;
if (!isIdentifierChar(text.at(col - 1)))
return false;
return col < text.size() && isIdentifierChar(text.at(col));
}
bool isAfterMemberAccess(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
if (col <= 0)
return false;
int i = col - 1;
while (i >= 0 && isIdentifierChar(text.at(i)))
--i;
if (i < 0)
return false;
const QChar c = text.at(i);
if (c == QLatin1Char('.'))
return true;
if (c == QLatin1Char('>') && i >= 1 && text.at(i - 1) == QLatin1Char('-'))
return true;
if (c == QLatin1Char(':') && i >= 1 && text.at(i - 1) == QLatin1Char(':'))
return true;
return false;
}
bool isFreshIndentedLine(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
if (col == 0)
return false;
const QString leftText = block.text().left(col);
for (const QChar &ch : leftText) {
if (!ch.isSpace())
return false;
}
return true;
}
bool isAfterEagerTrigger(const QTextCursor &cursor)
{
const QTextBlock block = cursor.block();
const int col = cursor.positionInBlock();
const QString text = block.text();
int i = col - 1;
while (i >= 0 && text.at(i).isSpace())
--i;
if (i < 0)
return false;
const QChar c = text.at(i);
return c == QLatin1Char('{') || c == QLatin1Char('(') || c == QLatin1Char(',')
|| c == QLatin1Char('=') || c == QLatin1Char('[') || c == QLatin1Char(';')
|| c == QLatin1Char(':') || c == QLatin1Char('>');
}
bool isManualMode()
{
return Settings::codeCompletionSettings().completionMode.stringValue() == "Manual";
}
} // anonymous namespace
QodeAssistClient::QodeAssistClient(LLMClientInterface *clientInterface)
: LanguageClient::Client(clientInterface)
, m_llmClient(clientInterface)
, m_recentCharCount(0)
{
setName("Qode Assist");
setName("QodeAssist");
LanguageClient::LanguageFilter filter;
filter.mimeTypes = QStringList() << "*";
setSupportedLanguage(filter);
@@ -56,11 +152,16 @@ QodeAssistClient::QodeAssistClient()
setupConnections();
m_typingTimer.start();
m_refactorHoverHandler = new RefactorSuggestionHoverHandler();
m_refactorWidgetHandler = new RefactorWidgetHandler(this);
}
QodeAssistClient::~QodeAssistClient()
{
cleanupConnections();
delete m_refactorHoverHandler;
delete m_refactorWidgetHandler;
}
void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
@@ -70,47 +171,81 @@ void QodeAssistClient::openDocument(TextEditor::TextDocument *document)
return;
Client::openDocument(document);
connect(document,
&TextDocument::contentsChangedWithPosition,
this,
[this, document](int position, int charsRemoved, int charsAdded) {
Q_UNUSED(charsRemoved)
if (!Settings::generalSettings().enableAutoComplete())
return;
auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project))
return;
auto editors = TextEditor::BaseTextEditor::textEditorsForDocument(document);
for (auto *editor : editors) {
if (auto *widget = editor->editorWidget()) {
widget->addHoverHandler(m_refactorHoverHandler);
widget->installEventFilter(this);
}
}
auto textEditor = BaseTextEditor::currentTextEditor();
if (!textEditor || textEditor->document() != document)
return;
connect(
document,
&TextDocument::contentsChangedWithPosition,
this,
[this, document](int position, int charsRemoved, int charsAdded) {
if (!Settings::codeCompletionSettings().autoCompletion())
return;
if (Settings::contextSettings().useProjectChangesCache())
ChangesManager::instance().addChange(document,
position,
charsRemoved,
charsAdded);
if (isManualMode())
return;
TextEditorWidget *widget = textEditor->editorWidget();
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
return;
const int cursorPosition = widget->textCursor().position();
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
auto project = ProjectManager::projectForFile(document->filePath());
if (!isEnabled(project))
return;
auto textEditor = BaseTextEditor::currentTextEditor();
if (!textEditor || textEditor->document() != document)
return;
if (Settings::codeCompletionSettings().useProjectChangesCache())
Context::ChangesManager::instance()
.addChange(document, position, charsRemoved, charsAdded);
TextEditorWidget *widget = textEditor->editorWidget();
if (widget->isReadOnly() || widget->multiTextCursor().hasMultipleCursors())
return;
const int cursorPosition = widget->textCursor().position();
if (cursorPosition < position || cursorPosition > position + charsAdded)
return;
if (charsRemoved > 0 || charsAdded <= 0) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
QTextCursor cursor = widget->textCursor();
cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1);
const QString lastChar = cursor.selectedText();
if (lastChar.isEmpty())
return;
const QChar lastCh = lastChar[0];
if (lastCh == QLatin1Char('\n') || lastCh == QChar::ParagraphSeparator
|| lastCh == QChar::LineSeparator) {
m_recentCharCount = 0;
m_typingTimer.restart();
return;
}
const bool isSpaceOrTab = lastCh.isSpace();
const bool ignoreWhitespace
= Settings::codeCompletionSettings().ignoreWhitespaceInCharCount();
if (!ignoreWhitespace || !isSpaceOrTab)
m_recentCharCount += charsAdded;
if (m_typingTimer.elapsed()
> Settings::generalSettings().autoCompletionTypingInterval()) {
m_recentCharCount = charsAdded;
m_typingTimer.restart();
}
if (m_typingTimer.elapsed()
> Settings::codeCompletionSettings().autoCompletionTypingInterval()) {
m_recentCharCount = (ignoreWhitespace && isSpaceOrTab) ? 0 : charsAdded;
m_typingTimer.restart();
}
if (m_recentCharCount > Settings::generalSettings().autoCompletionCharThreshold()) {
scheduleRequest(widget);
}
});
handleAutoRequestTrigger(widget);
});
}
bool QodeAssistClient::canOpenProject(ProjectExplorer::Project *project)
@@ -125,14 +260,36 @@ 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 auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
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);
@@ -142,77 +299,171 @@ 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);
if (m_runningRequests.contains(editor)) {
if (Settings::codeCompletionSettings().cancelOnInput())
cancelRunningRequest(editor);
else
return;
}
auto it = m_scheduledRequests.find(editor);
if (it == m_scheduledRequests.end()) {
auto timer = new QTimer(this);
timer->setSingleShot(true);
connect(timer, &QTimer::timeout, this, [this, editor]() {
if (editor
&& editor->textCursor().position()
== m_scheduledRequests[editor]->property("cursorPosition").toInt()
&& m_recentCharCount > Settings::generalSettings().autoCompletionCharThreshold())
requestCompletions(editor);
if (!editor || m_runningRequests.contains(editor))
return;
if (editor->textCursor().position()
!= m_scheduledRequests[editor]->property("cursorPosition").toInt())
return;
requestCompletions(editor);
});
connect(editor, &TextEditorWidget::destroyed, this, [this, editor]() {
delete m_scheduledRequests.take(editor);
cancelRunningRequest(editor);
});
connect(editor, &TextEditorWidget::cursorPositionChanged, this, [this, editor] {
cancelRunningRequest(editor);
});
it = m_scheduledRequests.insert(editor, timer);
}
it.value()->setProperty("cursorPosition", editor->textCursor().position());
it.value()->start(Settings::generalSettings().startSuggestionTimer());
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 auto &settings = Settings::codeCompletionSettings();
if (settings.abortAssistOnRequest() && !settings.respectQtcPopup())
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())
requestPosition = requestParams->position().toPositionInDocument(editor->document());
const MultiTextCursor cursors = editor->multiTextCursor();
if (cursors.hasMultipleCursors())
if (cursors.hasMultipleCursors() || cursors.hasSelection())
return;
if (cursors.hasSelection() || cursors.mainCursor().position() != requestPosition)
const int currentPosition = cursors.mainCursor().position();
if (requestPosition < 0 || currentPosition < requestPosition)
return;
QString typedSinceRequest;
if (currentPosition > requestPosition) {
QTextCursor diffCursor(editor->document());
diffCursor.setPosition(requestPosition);
diffCursor.setPosition(currentPosition, QTextCursor::KeepAnchor);
typedSinceRequest = diffCursor.selectedText();
if (typedSinceRequest.contains(QChar::ParagraphSeparator)
|| typedSinceRequest.contains(QLatin1Char('\n'))) {
return;
}
}
if (const std::optional<GetCompletionResponse> result = response.result()) {
auto isValidCompletion = [](const Completion &completion) {
return completion.isValid() && !completion.text().trimmed().isEmpty();
};
QList<Completion> completions = Utils::filtered(result->completions().toListOrEmpty(),
isValidCompletion);
QList<Completion> completions
= Utils::filtered(result->completions().toListOrEmpty(), isValidCompletion);
// remove trailing whitespaces from the end of the completions
QList<Completion> matchedCompletions;
matchedCompletions.reserve(completions.size());
for (Completion &completion : completions) {
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
QString completionText = completion.text();
const int end = int(completionText.size()) - 1;
int delta = 0;
while (delta <= end && completionText[end - delta].isSpace())
++delta;
if (delta > 0)
completion.setText(completionText.chopped(delta));
completionText.chop(delta);
if (!typedSinceRequest.isEmpty()) {
if (!completionText.startsWith(typedSinceRequest))
continue;
completionText = completionText.mid(typedSinceRequest.size());
if (completionText.isEmpty())
continue;
}
completion.setText(completionText);
matchedCompletions.append(completion);
}
if (completions.isEmpty())
if (matchedCompletions.isEmpty()) {
LOG_MESSAGE("No valid completions received");
return;
editor->insertSuggestion(
std::make_unique<LLMSuggestion>(completions.first(), editor->document()));
}
const Text::Position anchor = typedSinceRequest.isEmpty()
? Text::Position{}
: Text::Position::fromPositionInDocument(editor->document(), currentPosition);
const bool useAnchor = !typedSinceRequest.isEmpty();
auto suggestions = Utils::transform(matchedCompletions,
[useAnchor, &anchor](const Completion &c) {
auto toTextPos = [](const LanguageServerProtocol::Position pos) {
return Text::Position{pos.line() + 1, pos.character()};
};
if (useAnchor) {
return TextSuggestion::Data{Text::Range{anchor, anchor}, anchor, c.text()};
}
Text::Range range{toTextPos(c.range().start()), toTextPos(c.range().end())};
Text::Position pos{toTextPos(c.position())};
return TextSuggestion::Data{range, pos, c.text()};
});
editor->insertSuggestion(std::make_unique<LLMSuggestion>(suggestions, editor->document()));
}
}
@@ -221,13 +472,18 @@ void QodeAssistClient::cancelRunningRequest(TextEditor::TextEditorWidget *editor
const auto it = m_runningRequests.constFind(editor);
if (it == m_runningRequests.constEnd())
return;
m_progressHandler.hideProgress();
cancelRequest(it->id());
m_runningRequests.erase(it);
}
bool QodeAssistClient::isEnabled(ProjectExplorer::Project *project) const
{
return Settings::generalSettings().enableQodeAssist();
if (!project)
return Settings::generalSettings().enableQodeAssist();
Settings::ProjectSettings settings(project);
return settings.isEnabled();
}
void QodeAssistClient::setupConnections()
@@ -237,18 +493,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);
@@ -263,4 +514,215 @@ void QodeAssistClient::cleanupConnections()
m_scheduledRequests.clear();
}
void QodeAssistClient::handleRefactoringResult(const RefactorResult &result)
{
m_progressHandler.hideProgress();
if (!result.success) {
QString errorMessage = result.errorMessage.isEmpty()
? tr("Quick refactor failed")
: tr("Quick refactor failed: %1").arg(result.errorMessage);
if (result.editor) {
m_errorHandler.showError(result.editor, errorMessage);
}
LOG_MESSAGE(QString("Refactoring failed: %1").arg(result.errorMessage));
return;
}
if (!result.editor) {
LOG_MESSAGE("Refactoring result has no editor");
return;
}
int displayMode = Settings::quickRefactorSettings().displayMode();
if (displayMode == 0) {
displayRefactoringWidget(result);
} else {
displayRefactoringSuggestion(result);
}
}
void QodeAssistClient::displayRefactoringSuggestion(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
Utils::Text::Position pos = toTextPos(result.insertRange.begin);
int startPos = range.begin.toPositionInDocument(editorWidget->document());
int endPos = range.end.toPositionInDocument(editorWidget->document());
if (startPos != endPos) {
QTextCursor startCursor(editorWidget->document());
startCursor.setPosition(startPos);
if (startCursor.positionInBlock() > 0) {
startCursor.movePosition(QTextCursor::StartOfBlock);
}
QTextCursor endCursor(editorWidget->document());
endCursor.setPosition(endPos);
if (endCursor.positionInBlock() > 0) {
endCursor.movePosition(QTextCursor::EndOfBlock);
if (!endCursor.atEnd()) {
endCursor.movePosition(QTextCursor::NextCharacter);
}
}
Utils::Text::Position expandedBegin = Utils::Text::Position::fromPositionInDocument(
editorWidget->document(), startCursor.position());
Utils::Text::Position expandedEnd = Utils::Text::Position::fromPositionInDocument(
editorWidget->document(), endCursor.position());
range = Utils::Text::Range(expandedBegin, expandedEnd);
}
TextEditor::TextSuggestion::Data suggestionData{
Utils::Text::Range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)},
pos,
result.newText};
editorWidget->insertSuggestion(
std::make_unique<RefactorSuggestion>(suggestionData, editorWidget->document()));
m_refactorHoverHandler->setSuggestionRange(range);
m_refactorHoverHandler->setApplyCallback([this, editorWidget]() {
QKeyEvent tabEvent(QEvent::KeyPress, Qt::Key_Tab, Qt::NoModifier);
QApplication::sendEvent(editorWidget, &tabEvent);
m_refactorHoverHandler->clearSuggestionRange();
});
m_refactorHoverHandler->setDismissCallback([this, editorWidget]() {
editorWidget->clearSuggestion();
m_refactorHoverHandler->clearSuggestionRange();
});
LOG_MESSAGE("Displaying refactoring suggestion with hover handler");
}
void QodeAssistClient::displayRefactoringWidget(const RefactorResult &result)
{
TextEditorWidget *editorWidget = result.editor;
Utils::Text::Range range{toTextPos(result.insertRange.begin), toTextPos(result.insertRange.end)};
RefactorContext ctx = RefactorContextHelper::extractContext(editorWidget, range);
QString displayOriginal;
QString displayRefactored;
QString textToApply = result.newText;
if (ctx.isInsertion) {
bool isMultiline = result.newText.contains('\n');
if (isMultiline) {
displayOriginal = ctx.textBeforeCursor;
displayRefactored = ctx.textBeforeCursor + result.newText;
} else {
displayOriginal = ctx.textBeforeCursor + ctx.textAfterCursor;
displayRefactored = ctx.textBeforeCursor + result.newText + ctx.textAfterCursor;
}
if (!ctx.textBeforeCursor.isEmpty() || !ctx.textAfterCursor.isEmpty()) {
textToApply = result.newText;
}
} else {
displayOriginal = ctx.originalText;
displayRefactored = result.newText;
}
m_refactorWidgetHandler->setApplyCallback([this, editorWidget, result](const QString &editedText) {
applyRefactoringEdit(editorWidget, result.insertRange, editedText);
});
m_refactorWidgetHandler->setDeclineCallback([]() {});
m_refactorWidgetHandler->showRefactorWidget(
editorWidget, displayOriginal, displayRefactored, range,
ctx.contextBefore, ctx.contextAfter);
m_refactorWidgetHandler->setTextToApply(textToApply);
}
void QodeAssistClient::applyRefactoringEdit(TextEditor::TextEditorWidget *editor,
const Utils::Text::Range &range,
const QString &text)
{
const QTextCursor startCursor = range.begin.toTextCursor(editor->document());
const QTextCursor endCursor = range.end.toTextCursor(editor->document());
const int startPos = startCursor.position();
const int endPos = endCursor.position();
QTextCursor editCursor(editor->document());
editCursor.beginEditBlock();
if (startPos == endPos) {
bool isMultiline = text.contains('\n');
editCursor.setPosition(startPos);
if (isMultiline) {
editCursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
}
editCursor.insertText(text);
} else {
editCursor.setPosition(startPos);
editCursor.setPosition(endPos, QTextCursor::KeepAnchor);
editCursor.removeSelectedText();
editCursor.insertText(text);
}
editCursor.endEditBlock();
}
void QodeAssistClient::handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget)
{
const QTextCursor cursor = widget->textCursor();
const auto &settings = Settings::codeCompletionSettings();
const bool smart = settings.smartContextTrigger();
if (smart && (isInsideIdentifier(cursor) || isAfterMemberAccess(cursor)))
return;
const bool eager = smart && (isFreshIndentedLine(cursor) || isAfterEagerTrigger(cursor));
const int charThreshold = settings.autoCompletionCharThreshold();
if (eager || m_recentCharCount > charThreshold)
scheduleRequest(widget);
}
bool QodeAssistClient::eventFilter(QObject *watched, QEvent *event)
{
auto *editor = qobject_cast<TextEditor::TextEditorWidget *>(watched);
if (!editor)
return LanguageClient::Client::eventFilter(watched, event);
if (event->type() == QEvent::KeyPress) {
auto *keyEvent = static_cast<QKeyEvent *>(event);
if (keyEvent->key() == Qt::Key_Escape) {
if (m_runningRequests.contains(editor)) {
cancelRunningRequest(editor);
}
if (m_scheduledRequests.contains(editor)) {
auto *timer = m_scheduledRequests.value(editor);
if (timer && timer->isActive()) {
timer->stop();
}
}
if (m_refactorHandler && m_refactorHandler->isProcessing()) {
m_refactorHandler->cancelRequest();
}
m_progressHandler.hideProgress();
}
}
return LanguageClient::Client::eventFilter(watched, event);
}
} // namespace QodeAssist

View File

@@ -1,55 +1,57 @@
/*
* Copyright (C) 2023 The Qt Company Ltd.
* Copyright (C) 2024 Petr Mironychev
*
* This file is part of Qode Assist.
*
* The Qt Company portions:
* SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
*
* Petr Mironychev portions:
* QodeAssist is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QodeAssist is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QodeAssist. If not, see <https://www.gnu.org/licenses/>.
*/
// Copyright (C) 2023 The Qt Company Ltd.
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#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/EditorChatButtonHandler.hpp"
#include "widgets/RefactorWidgetHandler.hpp"
#include <languageclient/client.h>
#include <pluginllmcore/IPromptProvider.hpp>
#include <pluginllmcore/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());
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 applyRefactoringEdit(TextEditor::TextEditorWidget *editor, const Utils::Text::Range &range, const QString &text);
void handleAutoRequestTrigger(TextEditor::TextEditorWidget *widget);
QHash<TextEditor::TextEditorWidget *, GetCompletionRequest> m_runningRequests;
QHash<TextEditor::TextEditorWidget *, QTimer *> m_scheduledRequests;
@@ -58,6 +60,13 @@ private:
QElapsedTimer m_typingTimer;
int m_recentCharCount;
CompletionProgressHandler m_progressHandler;
CompletionErrorHandler m_errorHandler;
EditorChatButtonHandler m_chatButtonHandler;
QuickRefactorHandler *m_refactorHandler{nullptr};
RefactorSuggestionHoverHandler *m_refactorHoverHandler{nullptr};
RefactorWidgetHandler *m_refactorWidgetHandler{nullptr};
LLMClientInterface *m_llmClient;
};
} // namespace QodeAssist

View File

@@ -1,21 +1,5 @@
/*
* 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/>.
*/
// Copyright (C) 2024-2026 Petr Mironychev
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
@@ -24,54 +8,6 @@ namespace QodeAssist::Constants {
const char ACTION_ID[] = "QodeAssist.Action";
const char MENU_ID[] = "QodeAssist.Menu";
// settings
const char ENABLE_QODE_ASSIST[] = "QodeAssist.enableQodeAssist";
const char ENABLE_AUTO_COMPLETE[] = "QodeAssist.enableAutoComplete";
const char ENABLE_LOGGING[] = "QodeAssist.enableLogging";
const char LLM_PROVIDERS[] = "QodeAssist.llmProviders";
const char URL[] = "QodeAssist.url";
const char END_POINT[] = "QodeAssist.endPoint";
const char MODEL_NAME[] = "QodeAssist.modelName";
const char SELECT_MODELS[] = "QodeAssist.selectModels";
const char FIM_PROMPTS[] = "QodeAssist.fimPrompts";
const char TEMPERATURE[] = "QodeAssist.temperature";
const char MAX_TOKENS[] = "QodeAssist.maxTokens";
const char READ_FULL_FILE[] = "QodeAssist.readFullFile";
const char READ_STRINGS_BEFORE_CURSOR[] = "QodeAssist.readStringsBeforeCursor";
const char READ_STRINGS_AFTER_CURSOR[] = "QodeAssist.readStringsAfterCursor";
const char USE_TOP_P[] = "QodeAssist.useTopP";
const char TOP_P[] = "QodeAssist.topP";
const char USE_TOP_K[] = "QodeAssist.useTopK";
const char TOP_K[] = "QodeAssist.topK";
const char USE_PRESENCE_PENALTY[] = "QodeAssist.usePresencePenalty";
const char PRESENCE_PENALTY[] = "QodeAssist.presencePenalty";
const char USE_FREQUENCY_PENALTY[] = "QodeAssist.useFrequencyPenalty";
const char FREQUENCY_PENALTY[] = "QodeAssist.frequencyPenalty";
const char PROVIDER_PATHS[] = "QodeAssist.providerPaths";
const char START_SUGGESTION_TIMER[] = "QodeAssist.startSuggestionTimer";
const char AUTO_COMPLETION_CHAR_THRESHOLD[] = "QodeAssist.autoCompletionCharThreshold";
const char AUTO_COMPLETION_TYPING_INTERVAL[] = "QodeAssist.autoCompletionTypingInterval";
const char MAX_FILE_THRESHOLD[] = "QodeAssist.maxFileThreshold";
const char OLLAMA_LIVETIME[] = "QodeAssist.ollamaLivetime";
const char SPECIFIC_INSTRUCTIONS[] = "QodeAssist.specificInstractions";
const char MULTILINE_COMPLETION[] = "QodeAssist.multilineCompletion";
const char API_KEY[] = "QodeAssist.apiKey";
const char USE_SPECIFIC_INSTRUCTIONS[] = "QodeAssist.useSpecificInstructions";
const char USE_FILE_PATH_IN_CONTEXT[] = "QodeAssist.useFilePathInContext";
const char CUSTOM_JSON_TEMPLATE[] = "QodeAssist.customJsonTemplate";
const char USE_PROJECT_CHANGES_CACHE[] = "QodeAssist.useProjectChangesCache";
const char MAX_CHANGES_CACHE_SIZE[] = "QodeAssist.maxChangesCacheSize";
const char QODE_ASSIST_GENERAL_OPTIONS_ID[] = "QodeAssist.GeneralOptions";
const char QODE_ASSIST_GENERAL_SETTINGS_PAGE_ID[] = "QodeAssist.1GeneralSettingsPageId";
const char QODE_ASSIST_CONTEXT_SETTINGS_PAGE_ID[] = "QodeAssist.2ContextSettingsPageId";
const char QODE_ASSIST_PRESET_PROMPTS_SETTINGS_PAGE_ID[]
= "QodeAssist.3PresetPromptsSettingsPageId";
const char QODE_ASSIST_CUSTOM_PROMPT_SETTINGS_PAGE_ID[] = "QodeAssist.4CustomPromptSettingsPageId";
const char QODE_ASSIST_GENERAL_OPTIONS_CATEGORY[] = "QodeAssist.Category";
const char QODE_ASSIST_GENERAL_OPTIONS_DISPLAY_CATEGORY[] = "Qode Assist";
const char QODE_ASSIST_REQUEST_SUGGESTION[] = "QodeAssist.RequestSuggestion";
} // namespace QodeAssist::Constants

View File

@@ -1,33 +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/>.
*/
#pragma once
#include <QString>
namespace QodeAssist {
struct ContextData
{
QString prefix;
QString suffix;
QString instriuctions;
};
} // namespace QodeAssist

View File

@@ -1,68 +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/>.
*/
#pragma once
#include <QString>
#include <coreplugin/messagemanager.h>
#include <utils/qtcassert.h>
namespace QodeAssist {
inline bool &loggingEnabled()
{
static bool enabled = false;
return enabled;
}
inline void setLoggingEnabled(bool enable)
{
loggingEnabled() = enable;
}
inline void logMessage(const QString &message, bool silent = true)
{
if (!loggingEnabled())
return;
const QString prefixedMessage = QLatin1String("[Qode Assist] ") + message;
if (silent) {
Core::MessageManager::writeSilently(prefixedMessage);
} else {
Core::MessageManager::writeFlashing(prefixedMessage);
}
}
inline void logMessages(const QStringList &messages, bool silent = true)
{
if (!loggingEnabled())
return;
QStringList prefixedMessages;
for (const QString &message : messages) {
prefixedMessages << (QLatin1String("[Qode Assist] ") + message);
}
if (silent) {
Core::MessageManager::writeSilently(prefixedMessages);
} else {
Core::MessageManager::writeFlashing(prefixedMessages);
}
}
} // namespace QodeAssist

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