From 78660ce883a222c334276a454dee9cc050fc78d5 Mon Sep 17 00:00:00 2001 From: Avi Shefi Date: Tue, 26 Nov 2024 15:41:46 +0200 Subject: [PATCH 1/4] feat: Enhance commit message customization --- magit-gptcommit.el | 62 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/magit-gptcommit.el b/magit-gptcommit.el index e72bd6c..39f7f23 100644 --- a/magit-gptcommit.el +++ b/magit-gptcommit.el @@ -109,6 +109,19 @@ Now write Commit message in follow template: [label]:[one line of summary] : :group 'magit-gptcommit) +(defcustom magit-gptcommit-process-commit-message-function #'magit-gptcommit--process-commit-message + "Commit message function." + :type 'function + :group 'magit-gptcommit) + +(defcustom magit-gptcommit-commit-message-action 'replace + "Commit message action." + :type '(choice + (const append) + (const prepend) + (const replace)) + :group 'magit-gptcommit) + ;;; Cache (defvar magit-gptcommit-cache-limit 30 "Max number of cache entries.") @@ -368,6 +381,28 @@ NO-CACHE is non-nil if cache should be ignored." (delete-region start end)) (delete section (oref magit-root-section children)))) +(defun magit-gptcommit--process-commit-message (message orig-message) + "Process commit message based on generated MESSAGE and current commit message ORIG-MESSAGE. + +Executed in the context of the commit message buffer." + ;; Process the message but not the instructions at the end. + (save-restriction + (goto-char (point-min)) + (narrow-to-region + (point) + (if (re-search-forward (concat "^" comment-start) nil t) + (max 1 (- (point) 2)) + (point-max))) + (pcase magit-gptcommit-commit-message-action + ('append (goto-char (point-max))) + ('prepend (goto-char (point-min))) + (_ (delete-region (point-min) (point)))) ; defaults to 'replace + ;; Insert the new message. + (insert (string-trim message)) + (insert (or + (and (eq magit-gptcommit-commit-message-action 'prepend) " ") + "\n")))) + ;;;; Commit Message (defun magit-gptcommit-commit-accept () "Accept gptcommit message, after saving current message." @@ -375,22 +410,14 @@ NO-CACHE is non-nil if cache should be ignored." (when-let ((message (magit-repository-local-get 'magit-gptcommit--last-message)) (buf (magit-commit-message-buffer))) (with-current-buffer buf - ;; save the current non-empty and newly written comment, - ;; because otherwise it would be irreversibly lost. - (when-let ((message (git-commit-buffer-message))) - (unless (ring-member log-edit-comment-ring message) - (ring-insert log-edit-comment-ring message))) - ;; Delete the message but not the instructions at the end. - (save-restriction - (goto-char (point-min)) - (narrow-to-region - (point) - (if (re-search-forward (concat "^" comment-start) nil t) - (max 1 (- (point) 2)) - (point-max))) - (delete-region (point-min) (point))) - ;; Insert the new message. - (insert message)))) + (let (orig-message) + ;; save the current non-empty and newly written comment, + ;; because otherwise it would be irreversibly lost. + (when-let ((message (git-commit-buffer-message))) + (unless (ring-member log-edit-comment-ring message) + (ring-insert log-edit-comment-ring message) + (setq orig-message message))) + (funcall magit-gptcommit-process-commit-message-function message orig-message))))) (defun magit-gptcommit-commit-create () "Execute `magit-commit-create' and bring gptcommit message to editor." @@ -404,7 +431,8 @@ NO-CACHE is non-nil if cache should be ignored." "Accept gptcommit message and make a commit with current staged." (interactive) (if-let ((message (magit-repository-local-get 'magit-gptcommit--last-message))) - (magit-run-git "commit" "-m" message) + (magit-run-git "commit" "-m" + (funcall magit-gptcommit-process-commit-message-function message nil)) (user-error "No last gptcommit message found"))) ;;;; gptel From 2b304183b0df63b708c729f5f77cc58524ee73c1 Mon Sep 17 00:00:00 2001 From: Ahmed Shariff Date: Fri, 24 Jan 2025 22:07:31 -0800 Subject: [PATCH 2/4] refactor: Simplify response handling with gptel-request --- magit-gptcommit.el | 209 +++++++++------------------------------------ 1 file changed, 40 insertions(+), 169 deletions(-) diff --git a/magit-gptcommit.el b/magit-gptcommit.el index 39f7f23..03c969b 100644 --- a/magit-gptcommit.el +++ b/magit-gptcommit.el @@ -318,12 +318,23 @@ NO-CACHE is non-nil if cache should be ignored." (when worker (magit-gptcommit-abort)) (insert "\n") - (magit-gptcommit-gptel-get-response - key - (list :prompt (list (list :role "user" :content (format magit-gptcommit-prompt diff))) - :buffer buf - :position (point-marker)) - #'magit-gptcommit--stream-insert-response))) + (magit-repository-local-set 'magit-gptcommit--active-worker + (make-magit-gptcommit--worker + ;; NOTE: process is never used anywhere? + :key key)) + (gptel-request (format magit-gptcommit-prompt diff) + :callback #'magit-gptcommit--stream-insert-response + :buffer buf + :position (point-marker) + :fsm (gptel-make-fsm + :state 'INIT + :table gptel-request--transitions + :handlers '((WAIT gptel--handle-wait) + (ERRS magit-gptcommit--handle-errs gptel--fsm-last) + ;; FIXME: this is useful? + (TOOL gptel--handle-tool-use) + (TYPE magit-gptcommit--handle-type) + (DONE magit-gptcommit--handle-done gptel--fsm-last)))))) ;; store section in repository-local active worker (let ((section (car (last (oref magit-root-section children)))) (worker (magit-repository-local-get 'magit-gptcommit--active-worker))) @@ -436,169 +447,29 @@ Executed in the context of the commit message buffer." (user-error "No last gptcommit message found"))) ;;;; gptel -;;;;; modified from `gptel-curl-get-response' -(defun magit-gptcommit-gptel-get-response (key info callback) - "Retrieve response to prompt in INFO. - -KEY is a unique identifier for the worker. - -INFO is a plist with the following keys: -- :prompt (the prompt being sent) -- :buffer (the gptel buffer) -- :position (marker at which to insert the response). - -Call CALLBACK with the response and INFO afterwards. If omitted -the response is inserted into the current buffer after point." - (let* ((token (md5 (format "%s%s%s%s" - (random) (emacs-pid) (user-full-name) - (recent-keys)))) - (args (gptel-curl--get-args (plist-get info :prompt) token)) - (stream (and gptel-stream (gptel-backend-stream gptel-backend))) - (process (apply #'start-process "gptel-curl" - (generate-new-buffer "*gptel-curl*") "curl" args))) - (when (eq gptel-log-level 'debug) - (message "%S" args)) - ;; store process in repository-local variable - (magit-repository-local-set 'magit-gptcommit--active-worker - (make-magit-gptcommit--worker - :key key - :process process)) - (with-current-buffer (process-buffer process) - (set-process-query-on-exit-flag process nil) - (setf (alist-get process gptel-curl--process-alist) - (nconc (list :token token - ;; FIXME `aref' breaks `cl-struct' abstraction boundary - ;; FIXME `cl--generic-method' is an internal `cl-struct' - :parser (cl--generic-method-function - (if stream - ;; 调用 buffer 对应 backend 的 stream parser - (cl-find-method - 'gptel-curl--parse-stream nil - (list - (aref (buffer-local-value - 'gptel-backend (plist-get info :buffer)) - 0) t)) - (cl-find-method - 'gptel--parse-response nil - (list - ;; Emacs 中的 array 是字符串或 vector [1 2 3] - (aref (buffer-local-value - 'gptel-backend (plist-get info :buffer)) - 0) t t)))) - :callback (or callback - (if stream - #'gptel-curl--stream-insert-response - #'gptel--insert-response)) - :transformer nil) - info)) - (if stream - (progn (set-process-sentinel process #'magit-gptcommit--stream-cleanup) - (set-process-filter process #'magit-gptcommit--stream-filter)) - (set-process-sentinel process #'gptel-curl--sentinel))))) - -(defun magit-gptcommit--stream-cleanup (process _status) - "Process sentinel for GPTel curl requests. - -PROCESS and _STATUS are process parameters." - (let ((proc-buf (process-buffer process))) - (when (eq gptel-log-level 'debug) - (with-current-buffer proc-buf - (clone-buffer "*gptel-error*" 'show))) - (let* ((info (alist-get process gptel-curl--process-alist)) - (gptel-buffer (plist-get info :buffer)) - (backend-name - (gptel-backend-name - (buffer-local-value 'gptel-backend gptel-buffer))) - (tracking-marker (plist-get info :tracking-marker)) - (start-marker (plist-get info :position)) - (http-status (plist-get info :http-status)) - (http-msg (plist-get info :status)) - ;; (callback (plist-get info :callback)) - response-beg response-end) - (if (equal http-status "200") - (progn - ;; Finish handling response - (with-current-buffer (marker-buffer start-marker) - (setq response-beg (+ start-marker 2) - response-end (marker-position tracking-marker)) - (pulse-momentary-highlight-region response-beg tracking-marker)) - ;; (when gptel-mode (save-excursion (goto-char tracking-marker) - ;; (insert "\n\n" (gptel-prompt-prefix-string)))) - (with-current-buffer gptel-buffer - (magit-gptcommit--stream-update-status 'success))) - ;; Or Capture error message - (with-current-buffer proc-buf - (goto-char (point-max)) - (search-backward (plist-get info :token)) - (backward-char) - (pcase-let* ((`(,_ . ,header-size) (read (current-buffer))) - (json-object-type 'plist) - (response (progn (goto-char header-size) - (condition-case nil (json-read) - (json-readtable-error 'json-read-error)))) - (error-data (plist-get response :error))) - (cond - (error-data - (if (stringp error-data) - (message "%s error: (%s) %s" backend-name http-msg error-data) - (when-let ((error-msg (plist-get error-data :message))) - (message "%s error: (%s) %s" backend-name http-msg error-msg)) - (when-let ((error-type (plist-get error-data :type))) - (setq http-msg (concat "(" http-msg ") " (string-trim error-type)))))) - ((eq response 'json-read-error) - (message "ChatGPT error (%s): Malformed JSON in response." http-msg)) - (t (message "ChatGPT error (%s): Could not parse HTTP response." http-msg))))) - (with-current-buffer gptel-buffer - ;; tell callback error occurred - (magit-gptcommit--stream-update-status 'error http-msg))) - (with-current-buffer gptel-buffer - (run-hook-with-args 'gptel-post-response-functions response-beg response-end) - (setq-local magit-inhibit-refresh nil))) - (setf (alist-get process gptel-curl--process-alist nil 'remove) nil) - ;; clear repository-local variable - (magit-repository-local-delete 'magit-gptcommit--active-worker) - (kill-buffer proc-buf))) - -(defun magit-gptcommit--stream-filter (process output) - "Process filter for GPTel curl requests. -OUTPUT is the PROCESS stdout." - (let* ((proc-info (alist-get process gptel-curl--process-alist))) - (with-current-buffer (process-buffer process) - ;; Insert output - (save-excursion - (goto-char (process-mark process)) - (insert output) - (set-marker (process-mark process) (point))) - - ;; Find HTTP status - (unless (plist-get proc-info :http-status) - (save-excursion - (goto-char (point-min)) - (when-let* (((not (= (line-end-position) (point-max)))) - (http-msg (buffer-substring (line-beginning-position) - (line-end-position))) - (http-status - (save-match-data - (and (string-match "HTTP/[.0-9]+ +\\([0-9]+\\)" http-msg) - (match-string 1 http-msg))))) - (plist-put proc-info :http-status http-status) - (plist-put proc-info :status (string-trim http-msg)))) - ;; Run pre-response hook - (when (and (equal (plist-get proc-info :http-status) "200") - gptel-pre-response-hook) - (with-current-buffer (marker-buffer (plist-get proc-info :position)) - (run-hooks 'gptel-pre-response-hook)))) - - (when-let ((http-msg (plist-get proc-info :status)) - (http-status (plist-get proc-info :http-status))) - ;; Find data chunk(s) and run callback - (when-let (((equal http-status "200")) - (response (funcall (plist-get proc-info :parser) nil proc-info)) - ((not (equal response "")))) - (funcall (or (plist-get proc-info :callback) - #'gptel-curl--stream-insert-response) - response proc-info) - (magit-gptcommit--stream-update-status 'typing)))))) +(defun magit-gptcommit--handle-done (fsm) + (let* ((info (gptel-fsm-info fsm)) + (gptel-buffer (plist-get info :buffer)) + (tracking-marker (plist-get info :tracking-marker)) + (start-marker (plist-get info :position)) + response-beg response-end) + (with-current-buffer (marker-buffer start-marker) + (setq response-beg (+ start-marker 2) + response-end (marker-position tracking-marker)) + (pulse-momentary-highlight-region response-beg tracking-marker)) + (with-current-buffer gptel-buffer + (magit-gptcommit--stream-update-status 'success)) + (magit-repository-local-delete 'magit-gptcommit--active-worker))) + +(defun magit-gptcommit--handle-type (fsm) + (with-current-buffer (plist-get (gptel-fsm-info fsm) :buffer) + (magit-gptcommit--stream-update-status 'typing))) + +(defun magit-gptcommit--handle-errs (fsm) + (let ((info (gptel-fsm-info fsm))) + (with-current-buffer (plist-get info :buffer) + ;; tell callback error occurred + (magit-gptcommit--stream-update-status 'error (plist-get info :status))))) (defun magit-gptcommit--stream-insert-response (msg info) "Insert response in target section located by CONDITION. From a095b98d0d9162c9be78daa23c194c20b02385fd Mon Sep 17 00:00:00 2001 From: Ahmed Shariff Date: Mon, 21 Apr 2025 18:33:02 -0700 Subject: [PATCH 3/4] refactor: updating gptel functions to be in-line with llm function The #28 PR introduces some new functions and adds extensive debugging. Move the common function and adding appropriate debug calls within gptel functions as well. --- magit-gptcommit.el | 230 ++++++++++++++++++++++++++------------------- 1 file changed, 131 insertions(+), 99 deletions(-) diff --git a/magit-gptcommit.el b/magit-gptcommit.el index 4cb33c5..f522d92 100644 --- a/magit-gptcommit.el +++ b/magit-gptcommit.el @@ -774,6 +774,70 @@ ERROR-MSG is error message." (magit-gptcommit--debug "Error updating section status: %S" err) (message "Error updating section status: %S" err))))))))))) +(defun magit-gptcommit--backend-finalize () + "Finalize prompt response." + (magit-gptcommit--debug "Finalizing LLM prompt response, cleaning up worker") + ;; Try to update the section with the latest message before cleaning up + (magit-gptcommit--update-section-with-latest-message) + ;; Clean up the worker + (magit-repository-local-delete 'magit-gptcommit--active-worker) + + ;; Mark request as no longer in progress and update end time + (setq magit-gptcommit--request-in-progress nil) + (setq magit-gptcommit--last-request-end-time (float-time)) + (magit-gptcommit--debug "Request completed, marked as no longer in progress")) + +;; Add buffer-kill-hooks to abort gptcommit when magit buffers are killed +(defun magit-gptcommit--buffer-kill-hook () + "Abort gptcommit when a buffer is killed that might be part of the process." + (when-let* ((worker (magit-repository-local-get 'magit-gptcommit--active-worker)) + (sections (and worker (magit-gptcommit--worker-sections worker))) + (current-buf (current-buffer))) + ;; If this buffer is in the sections, abort the entire process + (when (assq current-buf sections) + (magit-gptcommit--debug "Magit buffer killed, aborting gptcommit process") + (magit-gptcommit-abort)))) + +(defun magit-gptcommit--insert-message (pos msg &optional buffer) + "Insert MSG at position POS in BUFFER or current buffer. +Ensures consistent message formatting across all insertion points." + (with-current-buffer (or buffer (current-buffer)) + (let ((inhibit-read-only t)) + (save-excursion + (goto-char pos) + (insert msg "\n\n"))))) + +(defun magit-gptcommit--update-section-with-latest-message () + "Update the current gptcommit section with the newest message available. +This ensures that even if the current worker was aborted, the newest message +from any worker is displayed in the section." + (magit-gptcommit--debug "Attempting to update section with latest message") + (condition-case err + (when-let* ((newest-msg (magit-gptcommit--find-newest-message)) + (section (magit-gptcommit--goto-target-section 'gptcommit))) + (with-slots (start content end) section + (when (and (markerp content) (markerp end) + (> (marker-position content) 1) + (> (marker-position end) (marker-position content))) + ;; Check current content to avoid unnecessary buffer modifications + (let* ((current-content (buffer-substring-no-properties + (marker-position content) + (marker-position end))) + (current-msg (string-trim current-content)) + (needs-update (not (string= current-msg newest-msg)))) + + (if needs-update + (progn + (magit-gptcommit--debug "Found valid section with different content, updating") + (let ((inhibit-read-only t)) + ;; Clear any existing content + (delete-region content end) + ;; Insert newest message using the helper function + (magit-gptcommit--insert-message content newest-msg))) + (magit-gptcommit--debug "Section already contains newest message, skipping update")))))) + (error + (magit-gptcommit--debug "Error updating section with latest message: %S" err)))) + ;;;; Handle backends (defun magit-gptcommit--ensure-backend-loaded () "Makes sure the backend is loaded. @@ -784,14 +848,23 @@ correctly this function will error." (pcase magit-gptcommit-backend ('llm (unless (featurep 'llm) + (magit-gptcommit--debug "Loading llm") (require 'llm)) (unless magit-gptcommit-llm-provider + (magit-gptcommit--debug "No llm provider, please configure `magit-gptcommit-llm-provider'.") (user-error "No llm provider, please configure `magit-gptcommit-llm-provider'."))) ('gptel (unless (featurep 'gptel) + (magit-gptcommit--debug "Loading gptel") (require 'gptel))) - (_ (user-error "Unknown backend `%s'" magit-gptcommit-backend))) - (file-missing (user-error "Failed to load backend `%s' with error %S. Is it installed?" + (_ + (magit-gptcommit--debug "Unknown backend `%s'" magit-gptcommit-backend) + (user-error "Unknown backend `%s'" magit-gptcommit-backend))) + (file-missing + (magit-gptcommit--debug "Failed to load backend `%s' with error %S. Is it installed?" + magit-gptcommit-backend + err) + (user-error "Failed to load backend `%s' with error %S. Is it installed?" magit-gptcommit-backend err)))) @@ -840,7 +913,7 @@ Calls CALLBACK with the prompt response and INFO to update the response." (error (magit-gptcommit--debug "Error in response callback: %S" err) (message "Error in response callback: %S" err))) - (magit-gptcommit--llm-finalize))) + (magit-gptcommit--backend-finalize))) (defun magit-gptcommit--llm-error-callback (err msg) "The error callback for llm." @@ -853,66 +926,13 @@ Calls CALLBACK with the prompt response and INFO to update the response." ;; Always ensure the request is properly finalized, even if errors occurred (condition-case err3 - (magit-gptcommit--llm-finalize) + (magit-gptcommit--backend-finalize) (error (magit-gptcommit--debug "Error in llm finalize: %S" err3) ;; Manually reset the request-in-progress flag as a last resort (setq magit-gptcommit--request-in-progress nil) (setq magit-gptcommit--last-request-end-time (float-time))))) -(defun magit-gptcommit--insert-message (pos msg &optional buffer) - "Insert MSG at position POS in BUFFER or current buffer. -Ensures consistent message formatting across all insertion points." - (with-current-buffer (or buffer (current-buffer)) - (let ((inhibit-read-only t)) - (save-excursion - (goto-char pos) - (insert msg "\n\n"))))) - -(defun magit-gptcommit--update-section-with-latest-message () - "Update the current gptcommit section with the newest message available. -This ensures that even if the current worker was aborted, the newest message -from any worker is displayed in the section." - (magit-gptcommit--debug "Attempting to update section with latest message") - (condition-case err - (when-let* ((newest-msg (magit-gptcommit--find-newest-message)) - (section (magit-gptcommit--goto-target-section 'gptcommit))) - (with-slots (start content end) section - (when (and (markerp content) (markerp end) - (> (marker-position content) 1) - (> (marker-position end) (marker-position content))) - ;; Check current content to avoid unnecessary buffer modifications - (let* ((current-content (buffer-substring-no-properties - (marker-position content) - (marker-position end))) - (current-msg (string-trim current-content)) - (needs-update (not (string= current-msg newest-msg)))) - - (if needs-update - (progn - (magit-gptcommit--debug "Found valid section with different content, updating") - (let ((inhibit-read-only t)) - ;; Clear any existing content - (delete-region content end) - ;; Insert newest message using the helper function - (magit-gptcommit--insert-message content newest-msg))) - (magit-gptcommit--debug "Section already contains newest message, skipping update")))))) - (error - (magit-gptcommit--debug "Error updating section with latest message: %S" err)))) - -(defun magit-gptcommit--llm-finalize () - "Finalize llm prompt response." - (magit-gptcommit--debug "Finalizing LLM prompt response, cleaning up worker") - ;; Try to update the section with the latest message before cleaning up - (magit-gptcommit--update-section-with-latest-message) - ;; Clean up the worker - (magit-repository-local-delete 'magit-gptcommit--active-worker) - - ;; Mark request as no longer in progress and update end time - (setq magit-gptcommit--request-in-progress nil) - (setq magit-gptcommit--last-request-end-time (float-time)) - (magit-gptcommit--debug "Request completed, marked as no longer in progress")) - (defun magit-gptcommit--llm-chat-streaming (key info callback) "Retrieve response to prompt in INFO. @@ -999,20 +1019,10 @@ See `magit-gptcommit--llm-chat-streaming' for parameter documentation." error-callback))) (magit-gptcommit--debug "Worker created and stored in repository-local variable"))) -;; Add buffer-kill-hooks to abort gptcommit when magit buffers are killed -(defun magit-gptcommit--buffer-kill-hook () - "Abort gptcommit when a buffer is killed that might be part of the process." - (when-let* ((worker (magit-repository-local-get 'magit-gptcommit--active-worker)) - (sections (and worker (magit-gptcommit--worker-sections worker))) - (current-buf (current-buffer))) - ;; If this buffer is in the sections, abort the entire process - (when (assq current-buf sections) - (magit-gptcommit--debug "Magit buffer killed, aborting gptcommit process") - (magit-gptcommit-abort)))) - ;;;; gptel (defun magit-gptcommit--handle-done (fsm) "Handler for gptel DONE state in FSM." + (magit-gptcommit--debug "gptel in DONE state") (let* ((info (gptel-fsm-info fsm)) (gptel-buffer (plist-get info :buffer)) (tracking-marker (plist-get info :tracking-marker)) @@ -1024,19 +1034,24 @@ See `magit-gptcommit--llm-chat-streaming' for parameter documentation." (pulse-momentary-highlight-region response-beg tracking-marker)) (with-current-buffer gptel-buffer (magit-gptcommit--stream-update-status 'success)) - (magit-repository-local-delete 'magit-gptcommit--active-worker))) + (magit-gptcommit--backend-finalize))) (defun magit-gptcommit--handle-type (fsm) "Handler for gptel TYPE state in FSM." + (magit-gptcommit--debug "gptel in TYPE state") (with-current-buffer (plist-get (gptel-fsm-info fsm) :buffer) (magit-gptcommit--stream-update-status 'typing))) (defun magit-gptcommit--handle-errs (fsm) "Handler for gptel ERRS state in FSM." (let ((info (gptel-fsm-info fsm))) + (magit-gptcommit--debug "gptel in ERRS state: %s %s" + (plist-get info :status) + (plist-get info :error)) (with-current-buffer (plist-get info :buffer) ;; tell callback error occurred - (magit-gptcommit--stream-update-status 'error (plist-get info :status))))) + (magit-gptcommit--stream-update-status 'error (plist-get info :status))) + (magit-gptcommit--backend-finalize))) (defun magit-gptcommit--gptel-request (key info callback) "Retrieve response to prompt in INFO. @@ -1050,35 +1065,52 @@ INFO is a plist with the following keys: - :tracking-marker (a marker that tracks the end of the inserted response text). Call CALLBACK with the response and INFO with partial and full responses." - (gptel-request (plist-get info :prompt) - :callback (lambda (response gptel-info) - (unless (plist-get gptel-info :tracking-marker) - (plist-put gptel-info :tracking-marker - (plist-get info :tracking-marker))) - (funcall callback response gptel-info)) - :buffer (plist-get info :buffer) - :position (plist-get info :position) - :fsm (gptel-make-fsm - :state 'INIT - :table `((INIT . ((t . WAIT))) - (WAIT . ((t . TYPE))) - (TYPE . ((,#'gptel--error-p . ERRS) - (,#'gptel--tool-use-p . TOOL) - (t . DONE))) - (TOOL . ((,#'gptel--error-p . ERRS) - (,#'gptel--tool-result-p . WAIT) - (t . DONE)))) - :handlers '((WAIT gptel--handle-wait) - (ERRS magit-gptcommit--handle-errs gptel--fsm-last) - ;; FIXME: this is useful? - (TOOL gptel--handle-tool-use) - (TYPE magit-gptcommit--handle-type) - (DONE magit-gptcommit--handle-done gptel--fsm-last)))) - - (magit-repository-local-set 'magit-gptcommit--active-worker - (make-magit-gptcommit--worker - ;; NOTE: process is never used anywhere? - :key key))) + (if magit-gptcommit--request-in-progress + (progn + (magit-gptcommit--debug "⚠️ Request blocked - another LLM request is in progress") + (message "Another GPT commit request is already in progress. Please wait...") + (when-let ((section (magit-gptcommit--goto-target-section 'gptcommit))) + (with-slots (start) section + (save-excursion + (goto-char (+ 12 start)) + (delete-region (point) (pos-eol)) + (insert (propertize "Waiting for previous request" 'font-lock-face 'warning)))))) + + (setq magit-gptcommit--request-in-progress t) + (magit-gptcommit--debug "Request marked as in progress") + + (magit-gptcommit--debug "Creating gptel request for buffer: %s" (plist-get info :buffer)) + (magit-gptcommit--debug "Using prompt length: %d characters" (length (plist-get info :prompt))) + (gptel-request (plist-get info :prompt) + :callback (lambda (response gptel-info) + (unless (plist-get gptel-info :tracking-marker) + (plist-put gptel-info :tracking-marker + (plist-get info :tracking-marker))) + (funcall callback response gptel-info)) + :buffer (plist-get info :buffer) + :position (plist-get info :position) + :fsm (gptel-make-fsm + :state 'INIT + :table `((INIT . ((t . WAIT))) + (WAIT . ((t . TYPE))) + (TYPE . ((,#'gptel--error-p . ERRS) + (,#'gptel--tool-use-p . TOOL) + (t . DONE))) + (TOOL . ((,#'gptel--error-p . ERRS) + (,#'gptel--tool-result-p . WAIT) + (t . DONE)))) + :handlers '((WAIT gptel--handle-wait) + (ERRS magit-gptcommit--handle-errs gptel--fsm-last) + ;; FIXME: this is useful? + (TOOL gptel--handle-tool-use) + (TYPE magit-gptcommit--handle-type) + (DONE magit-gptcommit--handle-done gptel--fsm-last)))) + + (magit-repository-local-set 'magit-gptcommit--active-worker + (make-magit-gptcommit--worker + ;; NOTE: process is never used anywhere? + :key key)) + (magit-gptcommit--debug "Worker created and stored in repository-local variable"))) ;;;; Footer From 0cb4713955b308332a385734937e705a119e549c Mon Sep 17 00:00:00 2001 From: Ahmed Shariff Date: Sun, 28 Sep 2025 17:10:15 -0700 Subject: [PATCH 4/4] refactor: Merge gptel handlers with gptel-request Require gptel-request and introduce a local handler alist so magit-gptcommit can merge its custom gptel handlers with the handlers provided by the gptel-request module. Instead of hardcoding the handler list, compute the unique set of handler keys from both alists and assemble a combined handler entry for each key, preserving magit-specific handlers while reusing gptel-request handlers. --- magit-gptcommit.el | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/magit-gptcommit.el b/magit-gptcommit.el index f522d92..ba16877 100644 --- a/magit-gptcommit.el +++ b/magit-gptcommit.el @@ -856,7 +856,8 @@ correctly this function will error." ('gptel (unless (featurep 'gptel) (magit-gptcommit--debug "Loading gptel") - (require 'gptel))) + (require 'gptel) + (require 'gptel-request))) (_ (magit-gptcommit--debug "Unknown backend `%s'" magit-gptcommit-backend) (user-error "Unknown backend `%s'" magit-gptcommit-backend))) @@ -1020,6 +1021,11 @@ See `magit-gptcommit--llm-chat-streaming' for parameter documentation." (magit-gptcommit--debug "Worker created and stored in repository-local variable"))) ;;;; gptel +(defvar magit-gptcommit--gptel-handlers '((WAIT gptel--handle-wait) + (ERRS magit-gptcommit--handle-errs gptel--fsm-last) + (TYPE magit-gptcommit--handle-type) + (DONE magit-gptcommit--handle-done gptel--fsm-last))) + (defun magit-gptcommit--handle-done (fsm) "Handler for gptel DONE state in FSM." (magit-gptcommit--debug "gptel in DONE state") @@ -1099,12 +1105,13 @@ Call CALLBACK with the response and INFO with partial and full responses." (TOOL . ((,#'gptel--error-p . ERRS) (,#'gptel--tool-result-p . WAIT) (t . DONE)))) - :handlers '((WAIT gptel--handle-wait) - (ERRS magit-gptcommit--handle-errs gptel--fsm-last) - ;; FIXME: this is useful? - (TOOL gptel--handle-tool-use) - (TYPE magit-gptcommit--handle-type) - (DONE magit-gptcommit--handle-done gptel--fsm-last)))) + :handlers + (let ((keys (-uniq (append (-map #'car gptel-request--handlers) + (-map #'car magit-gptcommit--gptel-handlers))))) + (cl-loop for key in keys + collect + `(,key ,@(-uniq (append (alist-get key magit-gptcommit--gptel-handlers) + (alist-get key gptel-request--handlers)))))))) (magit-repository-local-set 'magit-gptcommit--active-worker (make-magit-gptcommit--worker