1- local codecompanion_client = require (" codecompanion.http" )
2- local codecompanion_config = require (" codecompanion.config" )
31local codecompanion_adapter = require (" codecompanion.adapters" )
42local codecompanion_schema = require (" codecompanion.schema" )
53
64--- @class CodeCompanion.GitCommit.Generator
75local Generator = {}
86
9- --- @type string ?
10- local _adapter = nil
11- --- @type string ?
12- local _model = nil
7+ --- @type string ? Adapter name
8+ local _adapter_name = nil
9+ --- @type string ? Model name
10+ local _model_name = nil
1311
1412local CONSTANTS = {
1513 STATUS_ERROR = " error" ,
@@ -19,65 +17,178 @@ local CONSTANTS = {
1917--- @param adapter string ? The adapter to use for generation
2018--- @param model string ? The model of the adapter to use for generation
2119function Generator .setup (adapter , model )
22- _adapter = adapter
23- _model = model
20+ _adapter_name = adapter
21+ _model_name = model
2422end
2523
26- --- @param commit_history ? string[] Array of recent commit messages for context (optional )
27- function Generator .generate_commit_message (diff , lang , commit_history , callback )
28- -- Setup adapter with proper resolution
29- local adapter
30- local success , result = pcall (function ()
31- return codecompanion_adapter .resolve ({
32- name = _adapter ,
33- model = _model ,
34- })
35- end )
24+ --- Create a client for both HTTP and ACP adapters
25+ --- @param adapter table The resolved adapter
26+ --- @return table | nil client The client instance
27+ --- @return string | nil error Error message if failed
28+ local function create_client (adapter )
29+ if not adapter or not adapter .type then
30+ return nil , " Invalid adapter: missing type field"
31+ end
3632
37- if not success then
38- -- Fallback to simple resolve
39- adapter = codecompanion_adapter .resolve (_adapter )
33+ if adapter .type == " http" then
34+ local HTTPClient = require (" codecompanion.http" )
35+ return HTTPClient .new ({ adapter = adapter }), nil
36+ elseif adapter .type == " acp" then
37+ local ACPClient = require (" codecompanion.acp" )
38+ local client = ACPClient .new ({ adapter = adapter })
39+ local ok = client :connect_and_initialize ()
40+ if not ok then
41+ return nil , " Failed to connect and initialize ACP client"
42+ end
43+ return client , nil
4044 else
41- adapter = result
45+ return nil , " Unknown adapter type: " .. tostring ( adapter . type )
4246 end
47+ end
4348
44- if not adapter then
45- return callback (nil , " Failed to resolve adapter: " .. tostring (_adapter ))
46- end
49+ --- Send request using HTTP client
50+ --- @param client table HTTP client
51+ --- @param adapter table Adapter instance
52+ --- @param payload table Request payload
53+ --- @param callback function Callback function
54+ local function send_http_request (client , adapter , payload , callback )
55+ local accumulated = " "
56+ local has_error = false
4757
48- -- Configure adapter for non-streaming
49- adapter .opts = adapter .opts or {}
50- adapter .opts .stream = false
58+ -- Use async send to properly handle streaming responses
59+ client :send (payload , {
60+ stream = true ,
61+ on_chunk = function (chunk )
62+ if chunk and chunk ~= " " then
63+ -- Use adapter's chat_output handler to process the chunk
64+ local result = adapter .handlers .chat_output (adapter , chunk )
65+ if result and result .status == CONSTANTS .STATUS_SUCCESS then
66+ local content = result .output and result .output .content
67+ if content and content ~= " " then
68+ accumulated = accumulated .. content
69+ end
70+ end
71+ end
72+ end ,
73+ on_done = function ()
74+ if not has_error then
75+ if accumulated ~= " " then
76+ callback (vim .trim (accumulated ), nil )
77+ else
78+ callback (nil , " Generated content is empty" )
79+ end
80+ end
81+ end ,
82+ on_error = function (err )
83+ has_error = true
84+ local error_msg = " HTTP request failed: " .. (err .message or vim .inspect (err ))
85+ callback (nil , error_msg )
86+ end ,
87+ })
88+ end
5189
52- -- Map schema with model override if specified
53- local schema_opts = {}
54- if _model then
55- schema_opts .model = _model
56- end
57- adapter = adapter :map_schema_to_params (codecompanion_schema .get_default (adapter , schema_opts ))
90+ --- Send request using ACP client
91+ --- @param client table ACP client
92+ --- @param adapter table Adapter instance
93+ --- @param messages table Array of messages
94+ --- @param callback function Callback function
95+ local function send_acp_request (client , adapter , messages , callback )
96+ local accumulated = " "
97+ local has_error = false
98+
99+ -- ACP expects messages to have _meta field
100+ -- Add it to make messages compatible with form_messages
101+ local formatted_messages = vim .tbl_map (function (msg )
102+ return vim .tbl_extend (" force" , msg , {
103+ _meta = {
104+ visible = true ,
105+ },
106+ })
107+ end , messages )
58108
59- -- Create HTTP client
60- local new_client = codecompanion_client .new ({
61- adapter = adapter ,
109+ client
110+ :session_prompt (formatted_messages )
111+ :on_message_chunk (function (chunk )
112+ if chunk and chunk ~= " " then
113+ accumulated = accumulated .. chunk
114+ end
115+ end )
116+ :on_complete (function (stop_reason )
117+ if not has_error and accumulated ~= " " then
118+ -- ACP responses are plain text, wrap in expected format
119+ callback (vim .trim (accumulated ), nil )
120+ elseif not has_error then
121+ callback (nil , " ACP returned empty response" )
122+ end
123+ end )
124+ :on_error (function (error )
125+ has_error = true
126+ callback (nil , " ACP error: " .. vim .inspect (error ))
127+ end )
128+ :send ()
129+ end
130+
131+ --- @param commit_history ? string[] Array of recent commit messages for context (optional )
132+ function Generator .generate_commit_message (diff , lang , commit_history , callback )
133+ -- 1. Resolve adapter
134+ local adapter = codecompanion_adapter .resolve (_adapter_name , {
135+ model = _model_name ,
62136 })
137+ if not adapter then
138+ return callback (nil , " Failed to resolve adapter: " .. tostring (_adapter_name ))
139+ end
140+
141+ -- Validate adapter type
142+ if not adapter .type or (adapter .type ~= " http" and adapter .type ~= " acp" ) then
143+ return callback (nil , " Invalid or unsupported adapter type: " .. tostring (adapter .type ))
144+ end
63145
64- -- Create prompt for LLM
146+ -- 2. Create prompt
65147 local prompt = Generator ._create_prompt (diff , lang , commit_history )
66148
67- local payload = {
68- messages = adapter :map_roles ({
69- { role = " user" , content = prompt },
70- }),
149+ -- 3. Prepare messages
150+ local messages = {
151+ { role = " user" , content = prompt },
71152 }
72153
73- -- Send request to LLM
74- new_client :request (payload , {
75- callback = function (err , data , adapter )
76- Generator ._handle_response (err , data , adapter , callback )
77- end ,
78- }, {
79- silent = true ,
80- })
154+ -- 4. Handle HTTP and ACP adapters differently
155+ if adapter .type == " http" then
156+ -- Map schema for HTTP adapter
157+ local schema_opts = {}
158+ if _model_name then
159+ schema_opts .model = _model_name
160+ end
161+ adapter = adapter :map_schema_to_params (codecompanion_schema .get_default (adapter , schema_opts ))
162+
163+ -- Create client AFTER mapping schema
164+ -- This is important because HTTPClient.request() does vim.deepcopy(self.adapter)
165+ local client , err = create_client (adapter )
166+ if not client then
167+ return callback (nil , err )
168+ end
169+
170+ -- Prepare HTTP payload
171+ local payload = {
172+ messages = adapter :map_roles (messages ),
173+ }
174+
175+ send_http_request (client , adapter , payload , callback )
176+ elseif adapter .type == " acp" then
177+ -- Create ACP client
178+ local client , err = create_client (adapter )
179+ if not client then
180+ return callback (nil , err )
181+ end
182+
183+ -- ACP doesn't need schema mapping for simple prompts
184+ send_acp_request (client , adapter , messages , function (result , error )
185+ -- Disconnect after request completes
186+ pcall (function ()
187+ client :disconnect ()
188+ end )
189+ callback (result , error )
190+ end )
191+ end
81192end
82193
83194--- Create prompt for commit message generation
@@ -144,42 +255,4 @@ Return ONLY the commit message in the exact format shown above.]],
144255 )
145256end
146257
147- --- Handle LLM response
148- --- @param err table | nil Request error
149- --- @param data table | nil Response data
150- --- @param adapter table The adapter used
151- --- @param callback fun ( result : string | nil , error : string | nil ) Callback function
152- function Generator ._handle_response (err , data , adapter , callback )
153- -- Handle request errors
154- if err then
155- local error_msg = " Error generating commit message: " .. (err .stderr or err .message or " Unknown error" )
156- return callback (nil , error_msg )
157- end
158-
159- -- Check for empty/invalid data
160- if not data then
161- return callback (nil , " No response received from LLM" )
162- end
163-
164- -- Process response
165- if data then
166- local result = adapter .handlers .chat_output (adapter , data )
167- if result and result .status then
168- if result .status == CONSTANTS .STATUS_SUCCESS then
169- local content = result .output and result .output .content
170- if content and vim .trim (content ) ~= " " then
171- return callback (vim .trim (content ), nil )
172- else
173- return callback (nil , " Generated content is empty" )
174- end
175- elseif result .status == CONSTANTS .STATUS_ERROR then
176- local error_msg = result .output or " Unknown error occurred"
177- return callback (nil , error_msg )
178- end
179- end
180- end
181-
182- return callback (nil , " No valid response received" )
183- end
184-
185258return Generator
0 commit comments