-
-
Notifications
You must be signed in to change notification settings - Fork 406
Expand file tree
/
Copy pathinit.lua
More file actions
1729 lines (1502 loc) · 55.2 KB
/
init.lua
File metadata and controls
1729 lines (1502 loc) · 55.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
--=============================================================================
-- The Chat Buffer - Where all of the logic for conversing with an LLM sits
--=============================================================================
---@class CodeCompanion.Chat
---@field acp_connection? CodeCompanion.ACP.Connection The ACP session ID and connection
---@field adapter CodeCompanion.HTTPAdapter|CodeCompanion.ACPAdapter The adapter to use for the chat
---@field aug number The ID for the autocmd group
---@field buffer_context table The context of the buffer that the chat was initiated from
---@field buffer_diffs CodeCompanion.BufferDiffs Watch for any changes in buffers
---@field bufnr number The buffer number of the chat
---@field builder CodeCompanion.Chat.UI.Builder The builder for the chat UI
---@field callbacks table<string, fun(chat: CodeCompanion.Chat)[]> A table of callback functions that are executed at various points
---@field chat_parser vim.treesitter.LanguageTree The Markdown Tree-sitter parser for the chat buffer
---@field context CodeCompanion.Chat.Context
---@field context_items? table<CodeCompanion.Chat.Context> Context which is sent to the LLM e.g. buffers, slash command output
---@field current_request table|nil The current request being executed
---@field cycle number Records the number of turn-based interactions (User -> LLM) that have taken place
---@field create_buf fun(): number The function that creates a new buffer for the chat
---@field edit_tracker? CodeCompanion.Chat.EditTracker Edit tracking information for the chat
---@field from_prompt_library? boolean Whether the chat was initiated from the prompt library
---@field header_line number The line number of the user header that any Tree-sitter parsing should start from
---@field header_ns number The namespace for the virtual text that appears in the header
---@field id number The unique identifier for the chat
---@field intro_message? string The welcome message that is displayed in the chat buffer
---@field messages? CodeCompanion.Chat.Messages The messages in the chat buffer
---@field opts CodeCompanion.ChatArgs Store all arguments in this table
---@field settings? table The settings that are used in the adapter of the chat buffer
---@field subscribers table The subscribers to the chat buffer
---@field title? string The title of the chat buffer
---@field tokens? nil|number The number of tokens in the chat
---@field tools CodeCompanion.Tools The tools coordinator that executes available tools
---@field tool_orchestrator CodeCompanion.Tools.Orchestrator The current tool orchestrator
---@field tool_registry CodeCompanion.Chat.ToolRegistry Methods for handling interactions between the chat buffer and tools
---@field ui CodeCompanion.Chat.UI The UI of the chat buffer
---@field variables? CodeCompanion.Variables The variables available to the user
---@field window_opts? table Window configuration options for the chat buffer
---@field yaml_parser vim.treesitter.LanguageTree The Yaml Tree-sitter parser for the chat buffer
---@field _last_role string The last role that was rendered in the chat buffer
---@field _tool_monitors? table A table of tool monitors that are currently running in the chat buffer
---@class CodeCompanion.ChatArgs Arguments that can be injected into the chat
---@field acp_command? string The command to use to connect via ACP
---@field acp_session_id? string The ACP session ID which links to this chat buffer
---@field adapter? CodeCompanion.HTTPAdapter|CodeCompanion.ACPAdapter The adapter used in this chat buffer
---@field auto_submit? boolean Automatically submit the chat when the chat buffer is created
---@field buffer_context? table Context of the buffer that the chat was initiated from
---@field callbacks table<string, fun(chat: CodeCompanion.Chat)[]> A table of callback functions that are executed at various points
---@field from_prompt_library? boolean Whether the chat was initiated from the prompt library
---@field ignore_system_prompt? boolean Do not send the default system prompt with the request
---@field last_role string The last role that was rendered in the chat buffer-
---@field messages? CodeCompanion.Chat.Messages The messages to display in the chat buffer
---@field settings? table The settings that are used in the adapter of the chat buffer
---@field status? string The status of any running jobs in the chat buffe
---@field stop_context_insertion? boolean Stop any visual selection from being automatically inserted into the chat buffer
---@field title? string The title of the chat buffer
---@field tokens? table Total tokens spent in the chat buffer so far
---@field intro_message? string The welcome message that is displayed in the chat buffer
---@field window_opts? table Window configuration options for the chat buffer
local adapters = require("codecompanion.adapters")
local completion = require("codecompanion.providers.completion")
local config = require("codecompanion.config")
local edit_tracker = require("codecompanion.interactions.chat.edit_tracker")
local hash = require("codecompanion.utils.hash")
local helpers = require("codecompanion.interactions.chat.helpers")
local images_utils = require("codecompanion.utils.images")
local keymaps = require("codecompanion.utils.keymaps")
local log = require("codecompanion.utils.log")
local parser = require("codecompanion.interactions.chat.parser")
local schema = require("codecompanion.schema")
local utils = require("codecompanion.utils")
local api = vim.api
local fmt = string.format
local CONSTANTS = {
AUTOCMD_GROUP = "codecompanion.chat",
STATUS_CANCELLING = "cancelling",
STATUS_ERROR = "error",
STATUS_SUCCESS = "success",
BLANK_DESC = "[No messages]",
SYSTEM_PROMPT = [[You are an AI programming assistant named "CodeCompanion", working within the Neovim text editor.
You can answer general programming questions and perform the following tasks:
* Answer general programming questions.
* Explain how the code in a Neovim buffer works.
* Review the selected code from a Neovim buffer.
* Generate unit tests for the selected code.
* Propose fixes for problems in the selected code.
* Scaffold code for a new workspace.
* Find relevant code to the user's query.
* Propose fixes for test failures.
* Answer questions about Neovim.
* Prefer vim.api* methods where possible.
Follow the user's requirements carefully and to the letter.
Use the context and attachments the user provides.
Keep your answers short and impersonal, especially if the user's context is outside your core tasks.
Use Markdown formatting in your answers.
DO NOT use H1 or H2 headers in your response.
When suggesting code changes or new content, use Markdown code blocks.
To start a code block, use 4 backticks.
After the backticks, add the programming language name as the language ID and the file path within curly braces if available.
To close a code block, use 4 backticks on a new line.
If you want the user to decide where to place the code, do not add the file path.
In the code block, use a line comment with '...existing code...' to indicate code that is already present in the file. Ensure this comment is specific to the programming language.
Code block example:
````languageId {path/to/file}
// ...existing code...
{ changed code }
// ...existing code...
{ changed code }
// ...existing code...
````
Ensure line comments use the correct syntax for the programming language (e.g. "#" for Python, "--" for Lua).
For code blocks use four backticks to start and end.
Avoid wrapping the whole response in triple backticks.
Do not include diff formatting unless explicitly asked.
Do not include line numbers unless explicitly asked.
When given a task:
1. Think step-by-step and, unless the user requests otherwise or the task is very simple. For complex architectural changes, describe your plan in pseudocode first.
2. When outputting code blocks, ensure only relevant code is included, avoiding any repeating or unrelated code.
3. End your response with a short suggestion for the next user turn that directly supports continuing the conversation.
]],
}
local clients = {} -- Cache for HTTP and ACP clients
local llm_role = config.interactions.chat.roles.llm
local user_role = config.interactions.chat.roles.user
local show_settings = config.display.chat.show_settings
--=============================================================================
-- Private methods
--=============================================================================
---Add updated content from the pins to the chat buffer
---@param chat CodeCompanion.Chat
---@return nil
local function sync_all_buffer_content(chat)
local synced = vim
.iter(chat.context_items)
:filter(function(ctx)
return ctx.opts.sync_all
end)
:totable()
if vim.tbl_isempty(synced) then
return
end
for _, item in ipairs(synced) do
-- Don't add the item twice in the same cycle
local exists = false
vim.iter(chat.messages):each(function(msg)
if (msg.context and msg.context.id == item.id) and (msg._meta and msg._meta.cycle == chat.cycle) then
exists = true
end
end)
if not exists then
require(item.source)
.new({ Chat = chat })
:output({ path = item.path, bufnr = item.bufnr, params = item.params }, { item = true })
end
end
end
---Get the appropriate client for the adapter type
---@param adapter CodeCompanion.HTTPAdapter|CodeCompanion.ACPAdapter
---@return table
local function get_client(adapter)
if adapter.type == "acp" then
if not clients.acp then
clients.acp = require("codecompanion.acp")
end
return clients.acp
else
if not clients.http then
clients.http = require("codecompanion.http")
end
return clients.http
end
end
---Find a message in the table that has a specific tool call ID
---@param id string
---@param messages CodeCompanion.Chat.Messages
---@return table|nil
local function find_tool_call(id, messages)
for _, msg in ipairs(messages) do
if msg.tools and msg.tools.call_id and msg.tools.call_id == id then
return msg
end
end
return nil
end
---Increment the cycle count in the chat buffer
---@param chat CodeCompanion.Chat
---@return nil
local function increment_cycle(chat)
chat.cycle = chat.cycle + 1
end
---Make an id from a string or table
---@param val string|table
---@return number
local function make_id(val)
return hash.hash(val)
end
---Set the editable text area. This allows us to scope the Tree-sitter queries to a specific area
---@param chat CodeCompanion.Chat
---@param modifier? number
---@return nil
local function set_text_editing_area(chat, modifier)
modifier = modifier or 0
chat.header_line = api.nvim_buf_line_count(chat.bufnr) + modifier
end
---Ready the chat buffer for the next round of conversation
---@param chat CodeCompanion.Chat
---@param opts? table
---@return nil
local function ready_chat_buffer(chat, opts)
opts = opts or {}
if not opts.auto_submit and chat._last_role ~= config.constants.USER_ROLE then
increment_cycle(chat)
chat:add_buf_message({ role = config.constants.USER_ROLE, content = "" })
set_text_editing_area(chat, -2)
chat.ui:display_tokens(chat.chat_parser, chat.header_line)
chat.context:render()
chat:dispatch("on_ready")
end
chat:update_metadata()
-- If we're automatically responding to a tool output, we need to leave some
-- space for the LLM's response so we can then display the user prompt again
if opts.auto_submit then
chat.ui:add_line_break()
chat.ui:add_line_break()
end
log:info("Chat request finished")
chat:reset()
end
---Used to record the last chat buffer that was opened
---@type CodeCompanion.Chat|nil
---@diagnostic disable-next-line: missing-fields
local last_chat = {}
---Set the autocmds for the chat buffer
---@param chat CodeCompanion.Chat
---@return nil
local function set_autocmds(chat)
local bufnr = chat.bufnr
api.nvim_create_autocmd("BufEnter", {
group = chat.aug,
buffer = bufnr,
desc = "Log the most recent chat buffer",
callback = function()
last_chat = chat
end,
})
api.nvim_create_autocmd("CompleteDone", {
group = chat.aug,
buffer = bufnr,
callback = function()
local item = vim.v.completed_item
if item.user_data and item.user_data.type == "slash_command" then
-- Clear the word from the buffer
local row, col = unpack(api.nvim_win_get_cursor(0))
api.nvim_buf_set_text(bufnr, row - 1, col - #item.word, row - 1, col, { "" })
completion.slash_commands_execute(item.user_data, chat)
end
end,
})
if show_settings then
api.nvim_create_autocmd("CursorMoved", {
group = chat.aug,
buffer = bufnr,
desc = "Show settings information in the CodeCompanion chat buffer",
callback = function()
if chat.adapter.type ~= "http" then
return
end
local key_name, node = parser.get_settings_key(chat)
if not key_name or not node then
vim.diagnostic.set(config.INFO_NS, chat.bufnr, {})
return
end
local key_schema = chat.adapter.schema[key_name]
if key_schema and key_schema.desc then
local lnum, col, end_lnum, end_col = node:range()
local diagnostic = {
lnum = lnum,
col = col,
end_lnum = end_lnum,
end_col = end_col,
severity = vim.diagnostic.severity.INFO,
message = key_schema.desc,
}
vim.diagnostic.set(config.INFO_NS, chat.bufnr, { diagnostic })
end
end,
})
-- Validate the settings
api.nvim_create_autocmd("InsertLeave", {
group = chat.aug,
buffer = bufnr,
desc = "Parse the settings in the CodeCompanion chat buffer for any errors",
callback = function()
if chat.adapter.type ~= "http" then
return
end
local adapter = chat.adapter
---@cast adapter CodeCompanion.HTTPAdapter
local settings = parser.settings(bufnr, chat.yaml_parser, adapter)
local errors = schema.validate(adapter.schema, settings, adapter)
local node = settings.__ts_node
local items = {}
if errors and node then
for child in node:iter_children() do
assert(child:type() == "block_mapping_pair")
local key = vim.treesitter.get_node_text(child:named_child(0), chat.bufnr)
if errors[key] then
local lnum, col, end_lnum, end_col = child:range()
table.insert(items, {
lnum = lnum,
col = col,
end_lnum = end_lnum,
end_col = end_col,
severity = vim.diagnostic.severity.ERROR,
message = errors[key],
})
end
end
end
vim.diagnostic.set(config.ERROR_NS, chat.bufnr, items)
end,
})
end
-- Update metadata when ACP mode changes
api.nvim_create_autocmd("User", {
group = chat.aug,
pattern = "CodeCompanionChatACPModeChanged",
desc = "Update chat metadata when ACP mode changes",
callback = function(args)
if chat.acp_connection and args.data and args.data.session_id == chat.acp_connection.session_id then
chat:update_metadata()
end
end,
})
end
--=============================================================================
-- Public methods
--=============================================================================
---Methods that are available outside of CodeCompanion
---@type table<CodeCompanion.Chat>
local chatmap = {}
---@type table
_G.codecompanion_buffers = {}
---@type table
_G.codecompanion_chat_metadata = {}
---@class CodeCompanion.Chat
local Chat = {}
Chat.MESSAGE_TYPES = {
LLM_MESSAGE = "llm_message",
REASONING_MESSAGE = "reasoning_message",
SYSTEM_MESSAGE = "system_message",
TOOL_MESSAGE = "tool_message",
USER_MESSAGE = "user_message",
}
---@param args CodeCompanion.ChatArgs
---@return CodeCompanion.Chat
function Chat.new(args)
local id = math.random(10000000)
log:trace("Chat created with ID %d", id)
local self = setmetatable({
acp_session_id = args.acp_session_id or nil,
buffer_context = args.buffer_context,
callbacks = {},
context_items = {},
cycle = 1,
header_line = 1,
from_prompt_library = args.from_prompt_library or false,
id = id,
intro_message = args.intro_message or config.display.chat.intro_message,
messages = args.messages or {},
opts = args,
status = "",
title = args.title or nil,
create_buf = function()
local bufnr = api.nvim_create_buf(config.display.chat.window.buflisted, true)
api.nvim_buf_set_name(bufnr, fmt("[CodeCompanion] %d", bufnr))
-- Safely attach treesitter
vim.schedule(function()
pcall(vim.treesitter.start, bufnr)
end)
-- Set up omnifunc for automatic completion when no other completion provider is active
local completion_provider = config.interactions.chat.opts.completion_provider
if completion_provider == "default" then
vim.bo[bufnr].omnifunc = "v:lua.require'codecompanion.providers.completion.default.omnifunc'.omnifunc"
end
return bufnr
end,
_last_role = args.last_role or config.constants.USER_ROLE,
}, { __index = Chat })
---@cast self CodeCompanion.Chat
self.bufnr = self.create_buf()
self.aug = api.nvim_create_augroup(CONSTANTS.AUTOCMD_GROUP .. ":" .. self.bufnr, {
clear = false,
})
-- NOTE: Put the parser on the chat buffer for performance reasons
local ok, chat_parser, yaml_parser
ok, chat_parser = pcall(vim.treesitter.get_parser, self.bufnr, "markdown")
if not ok then
return log:error("[chat::init::new] Could not find the Markdown Tree-sitter parser")
end
self.chat_parser = chat_parser
if show_settings then
ok, yaml_parser = pcall(vim.treesitter.get_parser, self.bufnr, "yaml", { ignore_injections = false })
if not ok then
return log:error("Could not find the Yaml Tree-sitter parser")
end
self.yaml_parser = yaml_parser
end
table.insert(_G.codecompanion_buffers, self.bufnr)
chatmap[self.bufnr] = {
name = "Chat " .. vim.tbl_count(chatmap) + 1,
description = CONSTANTS.BLANK_DESC,
interaction = "chat",
chat = self,
}
if args.adapter and adapters.resolved(args.adapter) then
self.adapter = args.adapter
else
self.adapter = adapters.resolve(args.adapter or config.interactions.chat.adapter)
end
if not self.adapter then
return log:error("No adapter found")
end
utils.fire("ChatAdapter", {
adapter = adapters.make_safe(self.adapter),
bufnr = self.bufnr,
id = self.id,
})
utils.fire("ChatModel", {
adapter = adapters.make_safe(self.adapter),
bufnr = self.bufnr,
id = self.id,
model = self.adapter.schema and self.adapter.schema.model.default,
})
if self.adapter.type == "http" then
self:apply_settings(schema.get_default(self.adapter, args.settings))
elseif self.adapter.type == "acp" then
-- Initialize ACP connection early to receive available_commands_update
-- Connection happens asynchronously; commands can arrive 1-5 seconds later, at least on claude code
vim.schedule(function()
if args.acp_command then
self.adapter.commands.selected = self.adapter.commands[args.acp_command]
end
helpers.create_acp_connection(self)
end)
end
-- Initialize components
self.builder = require("codecompanion.interactions.chat.ui.builder").new({ chat = self })
self.context = require("codecompanion.interactions.chat.context").new({ chat = self })
self.subscribers = require("codecompanion.interactions.chat.subscribers").new()
self.tools = require("codecompanion.interactions.chat.tools").new({
adapter = self.adapter,
bufnr = self.bufnr,
messages = self.messages,
})
self.tool_registry = require("codecompanion.interactions.chat.tool_registry").new({ chat = self })
self.variables = require("codecompanion.interactions.chat.variables").new()
self.buffer_diffs = require("codecompanion.interactions.chat.buffer_diffs").new()
self.ui = require("codecompanion.interactions.chat.ui").new({
adapter = self.adapter,
aug = self.aug,
chat_id = self.id,
chat_bufnr = self.bufnr,
roles = { user = user_role, llm = llm_role },
settings = self.settings,
window_opts = args.window_opts,
})
self:update_metadata()
-- Likely this hasn't been set by the time the user opens the chat buffer
if not _G.codecompanion_current_context then
_G.codecompanion_current_context = self.buffer_context.bufnr
end
if args.messages then
self.messages = args.messages
end
self.close_last_chat()
self.ui:open():render(self.buffer_context, self.messages, { stop_context_insertion = args.stop_context_insertion })
-- Set the header line for the chat buffer
if args.messages and vim.tbl_count(args.messages) > 0 then
local header_line = parser.headers(self, self.chat_parser)
self.header_line = header_line and (header_line + 1) or 1
end
if vim.tbl_isempty(self.messages) or not helpers.has_user_messages(args.messages) then
self.ui:set_intro_msg(self.intro_message)
end
if config.interactions.chat.keymaps then
-- Filter out any private keymaps
local filtered_keymaps = {}
for k, v in pairs(config.interactions.chat.keymaps) do
if k:sub(1, 1) ~= "_" then
filtered_keymaps[k] = v
end
end
keymaps
.new({
bufnr = self.bufnr,
callbacks = require("codecompanion.interactions.chat.keymaps"),
data = self,
keymaps = filtered_keymaps,
})
:set()
end
local slash_command_keymaps = helpers.slash_command_keymaps(config.interactions.chat.slash_commands)
if vim.tbl_count(slash_command_keymaps) > 0 then
keymaps
.new({
bufnr = self.bufnr,
callbacks = require("codecompanion.interactions.chat.slash_commands.keymaps"),
data = self,
keymaps = slash_command_keymaps,
})
:set()
end
---@cast self CodeCompanion.Chat
self:set_system_prompt()
set_autocmds(self)
last_chat = self
for _, tool_name in pairs(config.interactions.chat.tools.opts.default_tools or {}) do
local tool_config = config.interactions.chat.tools[tool_name]
if tool_config ~= nil then
self.tool_registry:add(tool_name, tool_config)
elseif config.interactions.chat.tools.groups[tool_name] ~= nil then
self.tool_registry:add_group(tool_name, config.interactions.chat.tools)
end
end
-- Handle callbacks
if args.callbacks then
for event, callback_list in pairs(args.callbacks) do
if type(callback_list) == "function" then
-- Single callback
self:add_callback(event, callback_list)
elseif type(callback_list) == "table" then
-- Array of callbacks
for _, callback in ipairs(callback_list) do
self:add_callback(event, callback)
end
end
end
end
-- Set up subscriber callbacks
self:add_callback("on_ready", function(c)
c.subscribers:process(c)
end)
self:add_callback("on_cancelled", function(c)
c.subscribers:stop()
end)
self:add_callback("on_closed", function(c)
c.subscribers:stop()
end)
require("codecompanion.interactions.background.callbacks").register_chat_callbacks(self)
self:dispatch("on_created")
utils.fire("ChatCreated", { bufnr = self.bufnr, from_prompt_library = self.from_prompt_library, id = self.id })
if args.auto_submit then
self:submit()
end
return self ---@type CodeCompanion.Chat
end
---Add a callback for a specific event
---@param event string The event name
---@param callback fun(chat: CodeCompanion.Chat) The callback function
---@return CodeCompanion.Chat
function Chat:add_callback(event, callback)
if not self.callbacks[event] then
self.callbacks[event] = {}
end
if type(callback) == "function" then
table.insert(self.callbacks[event], callback)
end
return self
end
---Dispatch callbacks for a specific event
---@param event string The event name
---@param ... any Additional arguments to pass to callbacks
---@return CodeCompanion.Chat
function Chat:dispatch(event, ...)
local callbacks = self.callbacks[event]
if not callbacks then
return self
end
for _, callback in ipairs(callbacks) do
local ok, err = pcall(callback, self, ...)
if not ok then
log:error("Callback error for %s: %s", event, err, { silent = true })
end
end
return self
end
---Format and apply settings to the chat buffer
---@param settings? table
---@return CodeCompanion.Chat
function Chat:apply_settings(settings)
if self.adapter.type ~= "http" then
return self
end
self.settings = settings or schema.get_default(self.adapter)
return self
end
---Change the adapter in the chat buffer
---@param adapter string
function Chat:change_adapter(adapter)
local function fire()
return utils.fire("ChatAdapter", { bufnr = self.bufnr, adapter = adapters.make_safe(self.adapter) })
end
self.adapter = require("codecompanion.adapters").resolve(adapter)
self.ui.adapter = self.adapter
if self.adapter.type == "acp" then
-- We need to ensure the connection is created before proceeding so that
-- users are given a choice of models to select from
helpers.create_acp_connection(self)
end
self:set_system_prompt()
self:update_metadata()
self:apply_settings()
fire()
end
---Set a model in the chat buffer
---@param args { model?: string }
---@return CodeCompanion.Chat
function Chat:change_model(args)
local function apply()
return adapters.set_model({ acp_connection = self.acp_connection, adapter = self.adapter, model = args.model })
end
if self.adapter.type == "http" then
self.settings.model = args.model
self.adapter.schema.model.default = args.model
self.adapter = apply()
self:set_system_prompt()
self:apply_settings()
elseif self.adapter.type == "acp" then
apply()
end
self:update_metadata()
utils.fire("ChatModel", {
adapter = adapters.make_safe(self.adapter),
bufnr = self.bufnr,
model = args.model,
})
return self
end
---The source to provide the model entries for completion (cmp only)
---@param callback fun(request: table)
---@return nil
function Chat:complete_models(callback)
if self.adapter.type ~= "http" then
return
end
local items = {}
local cursor = api.nvim_win_get_cursor(0)
local key_name, node = parser.get_settings_key(self, { pos = { cursor[1] - 1, 1 } })
if not key_name or not node then
callback({ items = items, isIncomplete = false })
return
end
local key_schema = self.adapter.schema[key_name]
if key_schema.type == "enum" then
local choices = key_schema.choices
if type(choices) == "function" then
choices = choices(self.adapter)
end
for _, choice in ipairs(choices) do
table.insert(items, {
label = choice,
kind = require("cmp").lsp.CompletionItemKind.Keyword,
})
end
end
callback({ items = items, isIncomplete = false })
end
---@class CodeCompanion.SystemPrompt.Context
---@field language string
---@field adapter CodeCompanion.HTTPAdapter|CodeCompanion.ACPAdapter
---@field date string
---@field nvim_version string
---@field os string the operating system that the user is using
---@field default_system_prompt string
---@field cwd string current working directory
---@field project_root? string The closest parent directory that contains either a `.git`, `.svn`, or `.hg` directory
---@return CodeCompanion.SystemPrompt.Context
function Chat:make_system_prompt_ctx()
---@type table<string, fun(_chat: CodeCompanion.Chat):any>
local dynamic_ctx = {
-- These can be slow-to-run or too complex for a one-liner. So wrap them in
-- functions and use a metatable to handle the eval when needed.
adapter = function()
return vim.deepcopy(self.adapter)
end,
os = function()
local machine = vim.uv.os_uname().sysname
if machine == "Darwin" then
machine = "Mac"
end
if machine:find("Windows") then
machine = "Windows"
end
return machine
end,
}
local bufnr = self.bufnr
local winid = vim.fn.bufwinid(bufnr)
local static_ctx = { ---@type CodeCompanion.SystemPrompt.Context|{}
language = config.opts.language or "English",
date = tostring(os.date("%Y-%m-%d")),
nvim_version = vim.version().major .. "." .. vim.version().minor .. "." .. vim.version().patch,
cwd = vim.fn.getcwd(winid ~= -1 and winid or nil),
project_root = vim.fs.root(bufnr, { ".git", ".svn", "hg" }),
default_system_prompt = CONSTANTS.SYSTEM_PROMPT,
}
---@type CodeCompanion.SystemPrompt.Context
return setmetatable(static_ctx, {
__index = function(_, key)
local val = dynamic_ctx[key]
if type(val) == "function" then
return val()
end
return val
end,
})
end
---Set the system prompt in the chat buffer
---@param prompt? string
---@param opts? {opts: table, _meta: table, index?: number}
---@return CodeCompanion.Chat
function Chat:set_system_prompt(prompt, opts)
if self.opts and self.opts.ignore_system_prompt then
return self
end
prompt = prompt or config.interactions.chat.opts.system_prompt
opts = opts or { visible = false }
local _meta = { tag = "system_prompt_from_config" }
if opts._meta then
_meta = opts._meta
opts._meta = nil
end
-- If the system prompt already exists, update it
if helpers.has_tag(_meta.tag, self.messages) then
self:remove_tagged_message(_meta.tag)
end
-- Workout in the message stack the last system prompt is
local index
if not opts.index then
for i = #self.messages, 1, -1 do
if self.messages[i].role == config.constants.SYSTEM_ROLE then
index = i + 1
break
end
end
end
if prompt ~= "" then
if type(prompt) == "function" then
prompt = prompt(self:make_system_prompt_ctx())
end
local system_prompt = {
role = config.constants.SYSTEM_ROLE,
content = prompt,
}
system_prompt.opts = opts
_meta.cycle = self.cycle
_meta.id = make_id(system_prompt)
_meta.index = #self.messages + 1
system_prompt._meta = _meta
table.insert(self.messages, index or opts.index or 1, system_prompt)
end
return self
end
---Toggle the system prompt in the chat buffer
---@return nil
function Chat:toggle_system_prompt()
local has_system_prompt = vim.tbl_contains(
vim.tbl_map(function(msg)
return msg._meta and msg._meta.tag
end, self.messages),
"system_prompt_from_config"
)
if has_system_prompt then
self:remove_tagged_message("system_prompt_from_config")
utils.notify("Removed system prompt")
else
self:set_system_prompt()
utils.notify("Added system prompt")
end
end
---Remove a message with a given tag
---@param tag string
---@return nil
function Chat:remove_tagged_message(tag)
self.messages = vim
.iter(self.messages)
:filter(function(msg)
if msg._meta and msg._meta.tag == tag then
return false
end
return true
end)
:totable()
end
---Add a message to the message table
---@param data { role: string, content: string, reasoning?: CodeCompanion.Chat.Reasoning, tool_calls?: CodeCompanion.Chat.ToolCall[] }
---@param opts? table Options for the message
---@return CodeCompanion.Chat
function Chat:add_message(data, opts)
opts = opts or { visible = true }
if opts.visible == nil then
opts.visible = true
end
---@type CodeCompanion.Chat.Message
local message = {
role = data.role,
content = data.content,
reasoning = data.reasoning,
_meta = { id = 1, cycle = self.cycle },
}
-- Map tool_calls to tools.calls
if data.tool_calls then
message.tools = message.tools or {}
message.tools.calls = data.tool_calls
end
if opts._meta then
message._meta = vim.tbl_deep_extend("force", message._meta, opts._meta)
opts._meta = nil
end
if opts.context then
message.context = opts.context
opts.context = nil
end
message.opts = opts
message._meta.id = make_id(message)
message._meta.index = #self.messages + 1
if opts.index then
table.insert(self.messages, opts.index, message)
else
table.insert(self.messages, message)
end
return self
end
---Add an image to the chat buffer
---@param image CodeCompanion.Image The image object containing the path and other metadata
---@param opts? {role?: "user"|string, source?: string, bufnr?: integer} Options for adding the image
---@return nil
function Chat:add_image_message(image, opts)
opts = vim.tbl_deep_extend("force", {
role = config.constants.USER_ROLE,
source = "codecompanion.interactions.chat.slash_commands.image",
bufnr = image.bufnr,
}, opts or {})
local id = "<image>" .. (image.id or image.path) .. "</image>"
self:add_message({
role = opts.role,
content = image.base64,
}, {
context = { id = id, mimetype = image.mimetype, path = image.path or image.id },
_meta = { tag = "image" },
visible = false,
})
self.context:add({
bufnr = opts.bufnr,
id = id,
path = image.path,
source = opts.source,
})
end
---Apply any tools or variables that a user has tagged in their message
---@param message table
---@return nil
function Chat:replace_vars_and_tools(message)
if self.tools:parse(self, message) then
message.content = self.tools:replace(message.content)
end
if self.variables:parse(self, message) then
message.content = self.variables:replace(message.content, self.buffer_context.bufnr)
end
end
---Make a request to the LLM using the HTTP client
---@param payload table The payload to send to the LLM