Repository: chep/copilot-chat.el
Pull request #146: feat: add dedicated git commit model and improve commit message generation
Author: login: lroolle
Labels: name: enhancement
description: New feature or request
Created: 2025-04-01T07:05:00Z | Merged: 2025-04-17T07:37:58Z

The changes include:

* Add `copilot-chat-git-commit-model` and `copilot-chat-set-git-commit-model` for dedicated git commit model selection
* Improve commit message generation reliability with dedicated instance handling
* Ensure model policy validation before use
* Fix content accumulation order in git chat responses
* Enhance error handling and spinner behavior

This addresses the need for separate model configurations between regular chat operations and git commit message generation, allowing users to optimize for performance and cost by using different models for different operations.




Diff:
diff --git a/copilot-chat-command.el b/copilot-chat-command.el
index b7845bf..033da53 100644
--- a/copilot-chat-command.el
+++ b/copilot-chat-command.el
@@ -579,17 +579,26 @@ For example, GPT-3.5 has no more significance
 for most people nowadays than GPT-4o."
   (eq t (alist-get 'model_picker_enabled model)))
 
+(defun copilot-chat--model-enabled-p (model)
+  "Return non-nil if MODEL is enabled.
+The model is enabled if it has no policy or if its policy state is \"enabled\".
+This function checks the JSON policy data returned from the API."
+  (let ((policy (alist-get 'policy model)))
+    (or (not policy)
+        (equal (alist-get 'state policy) "enabled"))))
+
 (defun copilot-chat--get-model-choices-with-wait ()
   "Get the list of available models for Copilot Chat.
 waiting for fetch if needed.
 If models haven't been fetched yet and no cache exists,
 wait for the fetch to complete."
   (let ((models
-         (if copilot-chat-model-ignore-picker
-             (copilot-chat-connection-models copilot-chat--connection)
-           (seq-filter #'copilot-chat--model-picker-enabled
-                       (copilot-chat-connection-models
-                        copilot-chat--connection)))))
+         (seq-filter #'copilot-chat--model-enabled-p
+           (if copilot-chat-model-ignore-picker
+               (copilot-chat-connection-models copilot-chat--connection)
+             (seq-filter #'copilot-chat--model-picker-enabled
+                         (copilot-chat-connection-models
+                          copilot-chat--connection))))))
     (if models
         (let* ((model-info-list
                 (mapcar
@@ -679,7 +688,7 @@ wait for the fetch to complete."
 
 ;;;###autoload (autoload 'copilot-chat-set-model "copilot-chat" nil t)
 (defun copilot-chat-set-model (model)
-  "Set the Copilot Chat model to MODEL.
+  "Set the Copilot Chat model to MODEL for the current instance.
 Fetches available models from the API if not already fetched."
   (interactive
    (let* ((choices (copilot-chat--get-model-choices-with-wait))
@@ -706,10 +715,10 @@ Fetches available models from the API if not already fetched."
          (message "Setting model to: %s" model-value))
        (list model-value))))
 
-  ;; Set the model value
+  ;; Set the model value only for current instance
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    (message "Copilot Chat model set to %s for current instance" model)))
 
 (defun copilot-chat-yank()
   "Insert last code block given by `copilot-chat'."
diff --git a/copilot-chat-curl.el b/copilot-chat-curl.el
index c91d0a2..267f9ea 100644
--- a/copilot-chat-curl.el
+++ b/copilot-chat-curl.el
@@ -422,9 +422,9 @@ if the prompt is out of context."
   (setf (copilot-chat-curl-current-data instance) nil
         (copilot-chat-curl-answer instance) nil)
 
-  ;; Start the spinner animation
-  (copilot-chat--spinner-start instance)
-
+  ;; Start the spinner animation only for instances with chat buffers
+  (when (buffer-live-p (copilot-chat-chat-buffer instance))
+    (copilot-chat--spinner-start instance))
 
   (when (copilot-chat-curl-file instance)
     (delete-file (copilot-chat-curl-file instance)))
diff --git a/copilot-chat-git.el b/copilot-chat-git.el
index 6e556e5..63c31e3 100644
--- a/copilot-chat-git.el
+++ b/copilot-chat-git.el
@@ -30,6 +30,14 @@
 
 (require 'copilot-chat-copilot)
 
+(defcustom copilot-chat-commit-model nil
+  "The model to use specifically for commit message generation.
+When nil, falls back to `copilot-chat-default-model`.
+Set via `copilot-chat-set-commit-model'."
+  :type '(choice (const :tag "Use default model" nil)
+          (string :tag "Specific model"))
+  :group 'copilot-chat)
+
 (defcustom copilot-chat-ignored-commit-files
   '("pnpm-lock.yaml" "package-lock.json" "yarn.lock" "poetry.lock"
     "Cargo.lock" "go.sum" "composer.lock" "Gemfile.lock"
@@ -111,126 +119,178 @@ repository or if there are no staged changes.
        (apply #'copilot-chat--exec
               "git" "--no-pager" "diff" "--cached" "--" files-to-include)))))
 
+(defun copilot-chat--create-commit-instance ()
+  "Create a temporary, lightweight instance for commit message generation.
+This instance is specifically designed for generating commit messages
+without creating a chat buffer or setting up a full chat environment."
+  (let ((current-dir (file-name-directory (or (buffer-file-name) default-directory))))
+    (let ((instance (copilot-chat--make
+                     :directory current-dir
+                     :model (or copilot-chat-commit-model copilot-chat-default-model)
+                     :chat-buffer nil
+                     :first-word-answer t
+                     :history nil
+                     :buffers nil
+                     :prompt-history nil
+                     :prompt-history-position nil
+                     :yank-index 1
+                     :last-yank-start nil
+                     :last-yank-end nil
+                     :spinner-timer nil
+                     :spinner-index 0
+                     :spinner-status nil
+                     :curl-answer nil
+                     :curl-file nil
+                     :curl-current-data nil)))
+      ;; Store instance in our instances list to prevent GC issues with timers
+      (push instance copilot-chat--instances)
+      instance)))
+
+(defun copilot-chat--get-git-commit-template-comments ()
+  "Extract comments (lines starting with #) from the commit buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((comments ""))
+      (while (re-search-forward "^#.*$" nil t)
+        (setq comments (concat comments (match-string 0) "\n")))
+      comments)))
+
+(defun copilot-chat--commit-callback (instance current-buf start-pos accumulated-content template-comments wait-prompt)
+  "Callback function for handling commit message generation stream.
+INSTANCE is the copilot chat instance receiving data.
+CURRENT-BUF is the buffer where commit message will be inserted.
+START-POS is the position where commit message insertion begins.
+ACCUMULATED-CONTENT stores the progressively built message.
+TEMPLATE-COMMENTS contains the original Git commit template comments.
+WAIT-PROMPT is the temporary waiting message shown during generation."
+  (lambda (_cb-instance content)
+    (with-current-buffer current-buf
+      (save-excursion
+        (if (string= content copilot-chat--magic)
+            (progn
+              ;; Make sure to stop the spinner timer
+              (when (copilot-chat-spinner-timer instance)
+                (cancel-timer (copilot-chat-spinner-timer instance))
+                (setf (copilot-chat-spinner-timer instance) nil))
+
+              (with-current-buffer current-buf
+                (goto-char start-pos)
+                (when (looking-at wait-prompt)
+                  (delete-region start-pos (+ start-pos (length wait-prompt))))
+                ;; Restore the git commit template comments and insert generated message
+                (goto-char (point-max))
+                (delete-region (point-min) (point-max))
+                (insert accumulated-content "\n\n" template-comments))
+              (message "Commit message generation completed.")
+              (copilot-chat--debug 'commit "Generation completed successfully")
+
+              ;; Remove the temporary instance from instances list to avoid memory leaks
+              (setq copilot-chat--instances (delq instance copilot-chat--instances)))
+
+          (progn
+            (when (string= accumulated-content "")
+              (goto-char start-pos)
+              (when (looking-at wait-prompt)
+                (delete-region start-pos (+ start-pos (length wait-prompt)))))
+
+            (goto-char start-pos)
+            (delete-region start-pos (min (+ start-pos (length accumulated-content)) (point-max)))
+            (setq accumulated-content (concat accumulated-content content))
+            (insert accumulated-content)
+            (copilot-chat--debug 'commit "Received chunk: %d chars" (length content))))))))
+
 ;;;###autoload (autoload 'copilot-chat-insert-commit-message-when-ready "copilot-chat" nil t)
 (defun copilot-chat-insert-commit-message-when-ready ()
-  "Generate and insert a commit message using Copilot.
-Uses the current staged changes in git
-to generate an appropriate commit message.
-Requires the repository to have staged changes."
+  "Generate and insert a commit message using GitHub Copilot.
+Uses the current staged changes in git to generate an appropriate commit
+message.  Requires the repository to have staged changes ready for commit."
   (interactive)
-  ;; Get magit staged diff
   (aio-with-async
-    (let* ((instance (copilot-chat--current-instance))
+    (let* ((instance (copilot-chat--create-commit-instance))
            (current-buf (current-buffer))
            (start-pos (point))
-           (accumulated-content "")
-           (error-occurred nil)
            (diff (aio-await (copilot-chat--get-diff)))
-           (prompt (concat copilot-chat-commit-prompt diff))
-           ;; Store the git commit template comments
-           (template-comments
-            (save-excursion
-              (goto-char (point-min))
-              (let ((comments ""))
-                (while (re-search-forward "^#.*$" nil t)
-                  (setq comments (concat comments (match-string 0) "\n")))
-                comments)))
-           (wait-prompt "# [copilot-chat] Working on generating commit message, please wait... "))
-
-      ;; Debug messages using structured format
-      (copilot-chat--debug 'commit "Starting commit message generation")
+           (template-comments (copilot-chat--get-git-commit-template-comments))
+           (wait-prompt (format "# [copilot:%s] Generating commit message..." (copilot-chat-model instance)))
+           (accumulated-content ""))
+
+      (copilot-chat--debug 'commit "Starting direct commit message generation")
       (copilot-chat--debug 'commit "Diff size: %d bytes, Model: %s"
                            (length diff) (copilot-chat-model instance))
 
       (cond
        ((string-empty-p diff)
         (copilot-chat--debug 'commit "No changes found in staging area")
+        (setq copilot-chat--instances (delq instance copilot-chat--instances))
         (user-error "No staged changes found.  Please stage some changes first"))
 
        (t
-        (message "Generating commit message... Please wait.")
-
-        (insert wait-prompt)
-        (insert "\n\n")
-
+        (message "Generating commit message...")
+        (insert wait-prompt "\n\n")
         (goto-char start-pos)
 
-        (when (fboundp 'copilot-chat--spinner-start)
-          (copilot-chat--spinner-start instance))
-
-        ;; Ask Copilot with streaming response
-        (copilot-chat--ask
-         instance
-         prompt
-         (lambda (instance content)
-           (with-current-buffer current-buf
-             (save-excursion
-               (if (string= content copilot-chat--magic)
-                   ;; End of streaming
-                   (progn
-                     (when (fboundp 'copilot-chat--spinner-stop)
-                       (copilot-chat--spinner-stop instance))
-                     (with-current-buffer current-buf
-                       (goto-char start-pos)
-                       (when (looking-at wait-prompt)
-                         (delete-region start-pos (+ start-pos (length wait-prompt)))))
-                     (when (not error-occurred)
-                       ;; Move point to end of inserted text for convenience
-                       (goto-char (+ start-pos (length accumulated-content)))
-                       ;; Restore the git commit template comments
-                       (save-excursion
-                         (goto-char (point-max))
-                         (delete-region (point-min) (point-max))
-                         (insert accumulated-content "\n\n" template-comments))
-                       (copilot-chat--debug 'commit "Generation completed successfully"))
-                     (message "Commit message generation completed."))
-
-                 ;; Continue streaming
-                 (progn
-                   ;; If this is the first response chunk, update the UI accordingly
-                   (when (string= accumulated-content "")
-                     (if (fboundp 'copilot-chat--spinner-set-status)
-                         (copilot-chat--spinner-set-status instance "Generating")
-                       (message "Generating commit message...")))
-
-                   ;; Handle error messages
-                   (when (string-prefix-p "Error:" content)
-                     (setq error-occurred t)
-                     (message "%s" content))
-
-                   ;; Append the new content to our accumulator
-                   (setq accumulated-content (concat accumulated-content content))
-
-                   ;; Remove placeholder text on first content chunk
-                   (goto-char start-pos)
-                   (when (looking-at wait-prompt)
-                     (delete-region start-pos (+ start-pos (length wait-prompt))))
-
-                   ;; Go to where we need to insert/update content
-                   (goto-char start-pos)
-                   (delete-region start-pos (min (+ start-pos
-                                                    (length accumulated-content))
-                                                 (point-max)))
-                   (insert accumulated-content)
-
-                   (copilot-chat--debug 'commit "Received chunk: %d chars"
-                                        (length content)))))))
-         t))))))
+        (condition-case err
+            (copilot-chat--ask
+             instance
+             (concat copilot-chat-commit-prompt diff)
+             (copilot-chat--commit-callback
+              instance current-buf start-pos accumulated-content
+              template-comments wait-prompt)
+             t) ; out-of-context = t
+          (error
+           ;; Ensure the instance is cleaned up if an error occurs
+           (when (copilot-chat-spinner-timer instance)
+             (cancel-timer (copilot-chat-spinner-timer instance))
+             (setf (copilot-chat-spinner-timer instance) nil))
+           (setq copilot-chat--instances (delq instance copilot-chat--instances))
+           (signal (car err) (cdr err)))))))))
 
 ;;;###autoload (autoload 'copilot-chat-insert-commit-message "copilot-chat" nil t)
 (defun copilot-chat-insert-commit-message ()
   "Generate and insert a commit message using Copilot.
-Uses the current staged changes in git
-to generate an appropriate commit message.
-Requires the repository to have staged changes.
-This function is expected to be safe to open via magit
-when added to `git-commit-setup-hook'."
+Uses the current staged changes in git to generate an appropriate commit
+message.  Requires the repository to have staged changes.
+This function is expected to be safe to open via magit when added to
+`git-commit-setup-hook'."
   (interactive)
   ;;FIXME: I really don't want to do anything delayed by time,
   ;; but I had to in order to make it work anyway.
   ;; In fact, we would like to get rid of this kind of messy control.
   (run-with-timer 1 nil #'copilot-chat-insert-commit-message-when-ready))
 
+;;;###autoload (autoload 'copilot-chat-set-commit-model "copilot-chat" nil t)
+(defun copilot-chat-set-commit-model (model)
+  "Set the model to use specifically for commit message generation to MODEL.
+When called interactively, prompts for available models from the API."
+  (interactive
+   (let* ((choices (copilot-chat--get-model-choices-with-wait))
+          (max-id-width
+           (apply #'max
+                  (mapcar (lambda (choics)
+                            (length (cdr choics))) choices)))
+          ;; Create completion list with ID as prefix for unique identification
+          (completion-choices
+           (mapcar (lambda (choice)
+                     (let ((name (car choice))
+                           (id (cdr choice)))
+                       (cons (format (format "[%%-%ds] %%s" max-id-width)
+                                     id name)
+                             id)))
+                   choices))
+          (choice
+           (completing-read "Select commit message model: "
+                            (mapcar 'car completion-choices)
+                            nil t)))
+     ;; Extract model ID from the selected choice
+     (let ((model-value (cdr (assoc choice completion-choices))))
+       (when copilot-chat-debug
+         (message "Setting commit model to: %s" model-value))
+       (list model-value))))
+
+  ;; Set the commit model value
+  (setq copilot-chat-commit-model model)
+  (message "Commit message model set to %s" model))
+
 (provide 'copilot-chat-git)
 ;;; copilot-chat-git.el ends here
 
diff --git a/copilot-chat-request.el b/copilot-chat-request.el
index dc04af3..e9abac8 100644
--- a/copilot-chat-request.el
+++ b/copilot-chat-request.el
@@ -164,8 +164,9 @@ Argument PROMPT is the prompt to send to copilot.
 Argument CALLBACK is the function to call with copilot answer as argument.
 Argument OUT-OF-CONTEXT is a boolean to indicate
 if the prompt is out of context."
-  ;; Start spinner if available
-  (when (fboundp 'copilot-chat--spinner-start)
+  ;; Start spinner if available (only for instances with chat buffers)
+  (when (and (fboundp 'copilot-chat--spinner-start)
+             (buffer-live-p (copilot-chat-chat-buffer instance)))
     (copilot-chat--spinner-start instance))
 
   ;; Initialize answer accumulator
@@ -210,11 +211,11 @@ if the prompt is out of context."
                    ;; Update full response and call callback with final magic token
                    (setq full-response (concat full-response data))
                    (funcall callback instance data)
+                   (funcall callback instance copilot-chat--magic)
                    (unless out-of-context
                      (setf (copilot-chat-history instance)
                            (cons (list prompt "assistant")
-                                 (copilot-chat-history instance))))
-                   (funcall callback instance copilot-chat--magic)))
+                                 (copilot-chat-history instance))))))
       :status-code '((400 . (lambda (&rest _)
                               (when (fboundp 'copilot-chat--spinner-stop)
                                 (copilot-chat--spinner-stop instance))
diff --git a/copilot-chat-spinner.el b/copilot-chat-spinner.el
index c1f731f..53be62b 100644
--- a/copilot-chat-spinner.el
+++ b/copilot-chat-spinner.el
@@ -48,10 +48,14 @@
 (defun copilot-chat--get-spinner-buffer(instance)
   "Get Spinner buffer from the active frontend.
 Argument INSTANCE is the copilot chat instance to get the buffer for."
-  (let ((get-buffer-fn (copilot-chat-frontend-get-spinner-buffer-fn
-                        (copilot-chat--get-frontend))))
-    (when get-buffer-fn
-      (funcall get-buffer-fn instance))))
+  (condition-case err
+      (let ((get-buffer-fn (copilot-chat-frontend-get-spinner-buffer-fn
+                            (copilot-chat--get-frontend))))
+        (when (and get-buffer-fn
+                   (buffer-live-p (copilot-chat-chat-buffer instance)))
+          (funcall get-buffer-fn instance)))
+    (error
+     (when copilot-chat-debug (message "Error getting spinner buffer: %S" err)) nil)))
 
 (defun copilot-chat--spinner-start (instance)
   "Start the spinner animation in the Copilot Chat buffer.
@@ -101,11 +105,13 @@ Argument INSTANCE is the copilot chat instance to use."
     (cancel-timer (copilot-chat-spinner-timer instance))
     (setf (copilot-chat-spinner-timer instance) nil))
 
-  ;; Remove spinner overlay
-  (let ((buffer (copilot-chat--get-spinner-buffer instance)))
-    (when (and buffer (buffer-live-p buffer))
-      (with-current-buffer buffer
-        (remove-overlays (point-min) (point-max) 'copilot-chat-spinner t)))))
+  ;; Remove spinner overlay - with robust error handling
+  (condition-case err
+      (let ((buffer (copilot-chat--get-spinner-buffer instance)))
+        (when (and buffer (buffer-live-p buffer))
+          (with-current-buffer buffer
+            (remove-overlays (point-min) (point-max) 'copilot-chat-spinner t))))
+    (error (when copilot-chat-debug (message "Error stopping spinner: %S" err)))))
 
 (defun copilot-chat--spinner-set-status (instance status)
   "Set the status message to display with the spinner.
diff --git a/readme.org b/readme.org
index 7ed3c82..efb669c 100644
--- a/readme.org
+++ b/readme.org
@@ -172,6 +172,7 @@ You can call ~(copilot-chat-transient)~ to open transient menu. Almost all funct
 *** Troubleshooting
 - ~(copilot-chat-clear-auth-cache)~ clears the auth cache for Copilot Chat. You will have to login again.
 - ~(copilot-chat-reset-models)~: Resets the model cache and fetches models again. Useful when GitHub adds new models or updates model capabilities.
+- If you cannot see your expected models in the set-model menu, visit https://github.com/copilot and check if the model is enabled for your account.
 
 
 ** Customizable variables


Reviews:
user: login: chep
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-01T08:42:54Z
user: login: ncaq
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-01T10:32:52Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T03:45:59Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T03:50:56Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T06:25:09Z
user: login: chep
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-03T15:06:36Z
user: login: chep
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-03T15:15:12Z
user: login: ncaq
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T15:24:32Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T03:36:52Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T03:43:22Z
user: login: chep
state: COMMENTED
author_association: OWNER
submitted_at: 2025-04-09T07:39:40Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T08:24:12Z
user: login: ncaq
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-09T10:04:34Z
user: login: lroolle
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T09:23:08Z
user: login: ncaq
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-17T07:35:31Z

Review comments:
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: chep
body: I don't think it's a good idea to set default model.
For exemple, I have gpt 4o as default model because I often want quick answer. But sometimes, or for specific instance, I set it to something else and I don't want to change the default.
created_at: 2025-04-01T08:41:31Z
author_association: OWNER
diff_hunk: @@ -29,6 +29,7 @@
 (require 'aio)
 
 (require 'copilot-chat-copilot)
+(require 'copilot-chat-spinner)
path: copilot-chat-git.el
user: login: ncaq
body: Is this `require` statement really necessary?

This module is already being imported implicitly through other dependencies. 

While I understand some developers prefer to explicitly require all modules they might use, this practice can actually make it more difficult to identify truly unnecessary imports. (There might be static analysis tools for this that I'm not familiar with.)

In a project with this level of complexity, unnecessary imports make it harder to identify and resolve circular dependencies. 

For this reason, I'd prefer that we don't add require statements that aren't functionally necessary for the code to work.

created_at: 2025-04-01T10:26:58Z
author_association: COLLABORATOR
diff_hunk: @@ -102,111 +103,130 @@ repository or if there are no staged changes.
        (apply #'copilot-chat--exec
               "git" "--no-pager" "diff" "--cached" "--" files-to-include)))))
 
+(defun copilot-chat--create-commit-instance ()
+  "Create a temporary, lightweight instance for commit message generation.
+This instance is specifically designed for generating commit messages
+without creating a chat buffer or setting up a full chat environment."
+  (let ((current-dir (file-name-directory (or (buffer-file-name) default-directory))))
+    (let ((instance (copilot-chat--make
+                     :directory current-dir
+                     :model copilot-chat-default-model
+                     :chat-buffer nil
+                     :first-word-answer t
+                     :history nil
+                     :buffers nil
+                     :prompt-history nil
+                     :prompt-history-position nil
+                     :yank-index 1
+                     :last-yank-start nil
+                     :last-yank-end nil
+                     :spinner-timer nil
+                     :spinner-index 0
+                     :spinner-status nil
+                     :curl-answer nil
+                     :curl-file nil
+                     :curl-current-data nil)))
+      ;; Store instance in our instances list to prevent GC issues with timers
+      (push instance copilot-chat--instances)
+      instance)))
+
+(defun copilot-chat--get-git-commit-template-comments ()
+  "Extract comments (lines starting with #) from the commit buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((comments ""))
+      (while (re-search-forward "^#.*$" nil t)
+        (setq comments (concat comments (match-string 0) "\n")))
+      comments)))
+
+(defun copilot-chat--commit-callback (instance current-buf start-pos accumulated-content template-comments wait-prompt)
+  "Callback function for handling commit message generation stream.
+INSTANCE is the copilot chat instance receiving data.
+CURRENT-BUF is the buffer where commit message will be inserted.
+START-POS is the position where commit message insertion begins.
+ACCUMULATED-CONTENT stores the progressively built message.
+TEMPLATE-COMMENTS contains the original Git commit template comments.
+WAIT-PROMPT is the temporary waiting message shown during generation."
+  (lambda (cb-instance content)
+    (with-current-buffer current-buf
+      (save-excursion
+        (if (string= content copilot-chat--magic)
+            (progn
+              ;; Make sure to stop the spinner timer
+              (when (copilot-chat-spinner-timer instance)
+                (cancel-timer (copilot-chat-spinner-timer instance))
+                (setf (copilot-chat-spinner-timer instance) nil))
+
+              (with-current-buffer current-buf
+                (goto-char start-pos)
+                (when (looking-at wait-prompt)
+                  (delete-region start-pos (+ start-pos (length wait-prompt))))
+                ;; Restore the git commit template comments and insert generated message
+                (goto-char (point-max))
+                (delete-region (point-min) (point-max))
+                (insert accumulated-content "\n\n" template-comments))
+              (message "Commit message generation completed.")
+              (copilot-chat--debug 'commit "Generation completed successfully")
+
+              ;; Remove the temporary instance from instances list to avoid memory leaks
+              (setq copilot-chat--instances (delq instance copilot-chat--instances)))
+
+          (progn
+            (when (string= accumulated-content "")
+              (goto-char start-pos)
+              (when (looking-at wait-prompt)
+                (delete-region start-pos (+ start-pos (length wait-prompt)))))
+
+            (setq accumulated-content (concat accumulated-content content))
+
+            (goto-char start-pos)
+            (delete-region start-pos (min (+ start-pos (length accumulated-content)) (point-max)))
+            (insert accumulated-content)
+            (copilot-chat--debug 'commit "Received chunk: %d chars" (length content))))))))
+
 ;;;###autoload (autoload 'copilot-chat-insert-commit-message-when-ready "copilot-chat" nil t)
 (defun copilot-chat-insert-commit-message-when-ready ()
-  "Generate and insert a commit message using Copilot.
-Uses the current staged changes in git
-to generate an appropriate commit message.
-Requires the repository to have staged changes."
+  "Generate and insert a commit message using GitHub Copilot.
+Uses the current staged changes in git to generate an appropriate commit message.
+Requires the repository to have staged changes ready for commit."
   (interactive)
-  ;; Get magit staged diff
   (aio-with-async
-    (let* ((instance (copilot-chat--current-instance))
+    (let* ((instance (copilot-chat--create-commit-instance))
            (current-buf (current-buffer))
            (start-pos (point))
-           (accumulated-content "")
-           (error-occurred nil)
            (diff (aio-await (copilot-chat--get-diff)))
-           (prompt (concat copilot-chat-commit-prompt diff))
-           ;; Store the git commit template comments
-           (template-comments
-            (save-excursion
-              (goto-char (point-min))
-              (let ((comments ""))
-                (while (re-search-forward "^#.*$" nil t)
-                  (setq comments (concat comments (match-string 0) "\n")))
-                comments)))
-           (wait-prompt "# [copilot-chat] Working on generating commit message, please wait... "))
-
-      ;; Debug messages using structured format
-      (copilot-chat--debug 'commit "Starting commit message generation")
+           (template-comments (copilot-chat--get-git-commit-template-comments))
+           (wait-prompt (format "# [copilot:%s] Generating commit message..." (copilot-chat-model instance)))
path: copilot-chat-git.el
user: login: ncaq
body: This probably changes the loading indicator behavior, doesn't it?

It's not good practice to alter functionality in a PR labeled as a "refactor." People will review it assuming the behavior remains unchanged.

Looking back at the title, I see the type was actually "feat" (feature).

The better approach would be to first submit a PR that refactors the code without changing behavior, then follow up with a separate PR for the functionality changes.

created_at: 2025-04-01T10:32:00Z
author_association: COLLABORATOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: You’re right, forcing a default model update could override user-specific preferences and workflows, which isn’t ideal. 
created_at: 2025-04-02T03:45:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,111 +103,130 @@ repository or if there are no staged changes.
        (apply #'copilot-chat--exec
               "git" "--no-pager" "diff" "--cached" "--" files-to-include)))))
 
+(defun copilot-chat--create-commit-instance ()
+  "Create a temporary, lightweight instance for commit message generation.
+This instance is specifically designed for generating commit messages
+without creating a chat buffer or setting up a full chat environment."
+  (let ((current-dir (file-name-directory (or (buffer-file-name) default-directory))))
+    (let ((instance (copilot-chat--make
+                     :directory current-dir
+                     :model copilot-chat-default-model
+                     :chat-buffer nil
+                     :first-word-answer t
+                     :history nil
+                     :buffers nil
+                     :prompt-history nil
+                     :prompt-history-position nil
+                     :yank-index 1
+                     :last-yank-start nil
+                     :last-yank-end nil
+                     :spinner-timer nil
+                     :spinner-index 0
+                     :spinner-status nil
+                     :curl-answer nil
+                     :curl-file nil
+                     :curl-current-data nil)))
+      ;; Store instance in our instances list to prevent GC issues with timers
+      (push instance copilot-chat--instances)
+      instance)))
+
+(defun copilot-chat--get-git-commit-template-comments ()
+  "Extract comments (lines starting with #) from the commit buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((comments ""))
+      (while (re-search-forward "^#.*$" nil t)
+        (setq comments (concat comments (match-string 0) "\n")))
+      comments)))
+
+(defun copilot-chat--commit-callback (instance current-buf start-pos accumulated-content template-comments wait-prompt)
+  "Callback function for handling commit message generation stream.
+INSTANCE is the copilot chat instance receiving data.
+CURRENT-BUF is the buffer where commit message will be inserted.
+START-POS is the position where commit message insertion begins.
+ACCUMULATED-CONTENT stores the progressively built message.
+TEMPLATE-COMMENTS contains the original Git commit template comments.
+WAIT-PROMPT is the temporary waiting message shown during generation."
+  (lambda (cb-instance content)
+    (with-current-buffer current-buf
+      (save-excursion
+        (if (string= content copilot-chat--magic)
+            (progn
+              ;; Make sure to stop the spinner timer
+              (when (copilot-chat-spinner-timer instance)
+                (cancel-timer (copilot-chat-spinner-timer instance))
+                (setf (copilot-chat-spinner-timer instance) nil))
+
+              (with-current-buffer current-buf
+                (goto-char start-pos)
+                (when (looking-at wait-prompt)
+                  (delete-region start-pos (+ start-pos (length wait-prompt))))
+                ;; Restore the git commit template comments and insert generated message
+                (goto-char (point-max))
+                (delete-region (point-min) (point-max))
+                (insert accumulated-content "\n\n" template-comments))
+              (message "Commit message generation completed.")
+              (copilot-chat--debug 'commit "Generation completed successfully")
+
+              ;; Remove the temporary instance from instances list to avoid memory leaks
+              (setq copilot-chat--instances (delq instance copilot-chat--instances)))
+
+          (progn
+            (when (string= accumulated-content "")
+              (goto-char start-pos)
+              (when (looking-at wait-prompt)
+                (delete-region start-pos (+ start-pos (length wait-prompt)))))
+
+            (setq accumulated-content (concat accumulated-content content))
+
+            (goto-char start-pos)
+            (delete-region start-pos (min (+ start-pos (length accumulated-content)) (point-max)))
+            (insert accumulated-content)
+            (copilot-chat--debug 'commit "Received chunk: %d chars" (length content))))))))
+
 ;;;###autoload (autoload 'copilot-chat-insert-commit-message-when-ready "copilot-chat" nil t)
 (defun copilot-chat-insert-commit-message-when-ready ()
-  "Generate and insert a commit message using Copilot.
-Uses the current staged changes in git
-to generate an appropriate commit message.
-Requires the repository to have staged changes."
+  "Generate and insert a commit message using GitHub Copilot.
+Uses the current staged changes in git to generate an appropriate commit message.
+Requires the repository to have staged changes ready for commit."
   (interactive)
-  ;; Get magit staged diff
   (aio-with-async
-    (let* ((instance (copilot-chat--current-instance))
+    (let* ((instance (copilot-chat--create-commit-instance))
            (current-buf (current-buffer))
            (start-pos (point))
-           (accumulated-content "")
-           (error-occurred nil)
            (diff (aio-await (copilot-chat--get-diff)))
-           (prompt (concat copilot-chat-commit-prompt diff))
-           ;; Store the git commit template comments
-           (template-comments
-            (save-excursion
-              (goto-char (point-min))
-              (let ((comments ""))
-                (while (re-search-forward "^#.*$" nil t)
-                  (setq comments (concat comments (match-string 0) "\n")))
-                comments)))
-           (wait-prompt "# [copilot-chat] Working on generating commit message, please wait... "))
-
-      ;; Debug messages using structured format
-      (copilot-chat--debug 'commit "Starting commit message generation")
+           (template-comments (copilot-chat--get-git-commit-template-comments))
+           (wait-prompt (format "# [copilot:%s] Generating commit message..." (copilot-chat-model instance)))
path: copilot-chat-git.el
user: login: lroolle
body: yes its fits better as a feat
created_at: 2025-04-02T03:50:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: hi @chep i've add a new copilot-chat-user-set-model to persist user-chosen models across instances.
created_at: 2025-04-02T06:25:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -102,111 +102,130 @@ repository or if there are no staged changes.
        (apply #'copilot-chat--exec
               "git" "--no-pager" "diff" "--cached" "--" files-to-include)))))
 
+(defun copilot-chat--create-commit-instance ()
+  "Create a temporary, lightweight instance for commit message generation.
+This instance is specifically designed for generating commit messages
+without creating a chat buffer or setting up a full chat environment."
+  (let ((current-dir (file-name-directory (or (buffer-file-name) default-directory))))
+    (let ((instance (copilot-chat--make
+                     :directory current-dir
+                     :model (or copilot-chat-user-set-model copilot-chat-default-model)
+                     :chat-buffer nil
+                     :first-word-answer t
+                     :history nil
+                     :buffers nil
+                     :prompt-history nil
+                     :prompt-history-position nil
+                     :yank-index 1
+                     :last-yank-start nil
+                     :last-yank-end nil
+                     :spinner-timer nil
+                     :spinner-index 0
+                     :spinner-status nil
+                     :curl-answer nil
+                     :curl-file nil
+                     :curl-current-data nil)))
+      ;; Store instance in our instances list to prevent GC issues with timers
+      (push instance copilot-chat--instances)
+      instance)))
+
+(defun copilot-chat--get-git-commit-template-comments ()
+  "Extract comments (lines starting with #) from the commit buffer."
+  (save-excursion
+    (goto-char (point-min))
+    (let ((comments ""))
+      (while (re-search-forward "^#.*$" nil t)
+        (setq comments (concat comments (match-string 0) "\n")))
+      comments)))
+
+(defun copilot-chat--commit-callback (instance current-buf start-pos accumulated-content template-comments wait-prompt)
+  "Callback function for handling commit message generation stream.
+INSTANCE is the copilot chat instance receiving data.
+CURRENT-BUF is the buffer where commit message will be inserted.
+START-POS is the position where commit message insertion begins.
+ACCUMULATED-CONTENT stores the progressively built message.
+TEMPLATE-COMMENTS contains the original Git commit template comments.
+WAIT-PROMPT is the temporary waiting message shown during generation."
+  (lambda (cb-instance content)
+    (with-current-buffer current-buf
+      (save-excursion
+        (if (string= content copilot-chat--magic)
+            (progn
+              ;; Make sure to stop the spinner timer
+              (when (copilot-chat-spinner-timer instance)
+                (cancel-timer (copilot-chat-spinner-timer instance))
+                (setf (copilot-chat-spinner-timer instance) nil))
+
+              (with-current-buffer current-buf
+                (goto-char start-pos)
+                (when (looking-at wait-prompt)
+                  (delete-region start-pos (+ start-pos (length wait-prompt))))
+                ;; Restore the git commit template comments and insert generated message
+                (goto-char (point-max))
+                (delete-region (point-min) (point-max))
+                (insert accumulated-content "\n\n" template-comments))
+              (message "Commit message generation completed.")
+              (copilot-chat--debug 'commit "Generation completed successfully")
+
+              ;; Remove the temporary instance from instances list to avoid memory leaks
+              (setq copilot-chat--instances (delq instance copilot-chat--instances)))
+
+          (progn
+            (when (string= accumulated-content "")
+              (goto-char start-pos)
+              (when (looking-at wait-prompt)
+                (delete-region start-pos (+ start-pos (length wait-prompt)))))
+
+            (setq accumulated-content (concat accumulated-content content))
+
+            (goto-char start-pos)
+            (delete-region start-pos (min (+ start-pos (length accumulated-content)) (point-max)))
path: copilot-chat-git.el
user: login: chep
body: Should'nt this be done before setting `accumulated-content` ? Here we are using the new length but the buffer contains old value of `accumulated-content`.
created_at: 2025-04-03T14:59:43Z
author_association: OWNER
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: chep
body: I don't understand this variable. What's the difference with default model ?
It means that we must remember the last set model when creating an instance, instead of being sure that model will be the default one.
For commit messages, I'm not happy with the current behavior, or this one, as we can't select the model. Currently we are using the model of current instance (if any) and with your modifications we will use the last one set. It can be a problem if the model has a low request limit.
I think the best thing is to create a custom variable `copilot-chat-commit-model` (or something like that) with a `copilot-chat-set-commit-model` function.

What do you think ?
created_at: 2025-04-03T15:15:12Z
author_association: OWNER
diff_hunk: @@ -36,12 +36,19 @@ Use `copilot-chat-set-model' to interactively select a model."
   :type 'string
   :group 'copilot-chat)
 
+(defcustom copilot-chat-user-set-model nil
+  "User explicitly set model that overrides the default.
+When non-nil, this value will be used instead of `copilot-chat-default-model'
+when creating new instances. Set via `copilot-chat-set-model'."
+  :type 'string
+  :group 'copilot-chat)
+
path: copilot-chat-instance.el
user: login: ncaq
body: I don't see the purpose of separating `copilot-chat-default-model` and `copilot-chat-user-set-model`, since the default model isn't actually a constant defined with `defconst` - it's a custom variable that users can configure.

Was there a miscommunication somewhere? I think the intention wasn't about not modifying the default value.

With this approach, we still have the same issue that was originally pointed out - if you change the model for Git operations, it changes the model for everything.

I believe a better solution would be to create a dedicated custom variable for the default Git commit model, and then reference that during initialization when creating Git instances.
created_at: 2025-04-03T15:24:23Z
author_association: COLLABORATOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: I understand your concerns about model selection. Let me clarify the reasoning behind `copilot-chat-user-set-model`:

Initially, I wanted to address the git commit workflow where a new instance is created and needed to use an appropriate model. I first considered adding a specific `copilot-chat-commit-model` variable.

However, I realized that preserving the user's most recently selected model across instances would provide a more intuitive experience in most cases. The `user-set-model` variable serves as a "session preference" that differs from the configured default but persists across new instances.

This approach solves the commit model selection issue while also maintaining consistent behavior for users who temporarily switch models and expect that choice to persist to new instances.

I'm open to implementing a dedicated `copilot-chat-commit-model` variable instead if you think that would better address your concerns about different contexts having different model needs.
created_at: 2025-04-07T03:36:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: @ncaq FYI
created_at: 2025-04-07T03:43:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: chep
body: I think it would be better to use a `copilot-chat-commit-model` variable. This would provide a more predictable result for the user.  
I am planning to implement a new feature soon: setting `copilot-chat-default-model` to `nil` would allow the user to choose the model interactively each time a new instance is created. I could then make the necessary changes for `copilot-chat-commit-model` as well.
created_at: 2025-04-09T07:39:39Z
author_association: OWNER
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: I think having a dedicated `copilot-chat-commit-model` makes sense. It separates concerns better and gives users more predictable control.

However, the current workflow where you need to choose directory and then set model separately feels a bit clunky. Ideally, copilot-chat should be ready immediately with sensible defaults, and let users customize settings later if needed.

I can implement the set-commit-mode part if you'd like - adding the new variable and corresponding function would be straightforward.

created_at: 2025-04-09T08:24:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: ncaq
body: I wonder if you wanted something like the web API's sessionStorage, which persists during the current Emacs session but disappears after restarting? Even then, since variable settings in Emacs typically aren't preserved without explicit configuration, I don't think we need separate layers.
I still believe having a dedicated commit message model is more intuitive and practical. Most users won't frequently switch models for each commit message generation (e.g., "use o3-mini for this commit, then Claude 3.7 for the next...").
Users will likely have their preference (whether it's faster GPT-4o, more consistent o3-mini, or Claude 3.7 with higher token limits), but once they set it in their init.el, they probably won't change it until GitHub significantly updates their model lineup.
The only common use case I can imagine for switching would be when someone normally uses o3-mini but wants Claude 3.7 or Gemini for a particularly large diff. In that scenario, I'd personally want to return to my default model after generating that special commit message.
created_at: 2025-04-09T10:04:34Z
author_association: COLLABORATOR
diff_hunk: @@ -704,7 +704,9 @@ Fetches available models from the API if not already fetched."
   ;; Set the model value
   (let ((instance (copilot-chat--current-instance)))
     (setf (copilot-chat-model instance) model)
-    (message "Copilot Chat model set to %s" model)))
+    ;; Also update the default model setting
+    (setq-default copilot-chat-default-model model)
path: copilot-chat-command.el
user: login: lroolle
body: The main issue here is that once a model is selected, we should remember that choice for any new instance. It’s kind of expected that the last-picked model just becomes the default moving forward.

Perhaps caching this to a file might be even better, then we’d keep that preference even after restarting Emacs. 

created_at: 2025-04-10T09:23:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: chep
created_at: 2025-04-03T15:07:54Z
body: @ncaq There is nothing in the check tab, only `Workflow runs completed with no jobs`. Is it ok ?
author_association: OWNER
user: login: ncaq
created_at: 2025-04-03T15:29:18Z
body: > @ncaq There is nothing in the check tab, only `Workflow runs completed with no jobs`. Is it ok ?

I don't really see why the actions aren't executing either. It's scary...
Since my computer is broken and it's almost 1 AM Japan time, I'll look into it later.
author_association: COLLABORATOR
user: login: ncaq
created_at: 2025-04-07T09:02:27Z
body: @chep 

> @ncaq There is nothing in the check tab, only `Workflow runs completed with no jobs`. Is it ok ?

It's been a while since I added GitHub Actions configuration to a public open-source repository, so I wasn't familiar with the correct settings. I've fixed it in the following PR.

[ci: add trigger `pull_request` by ncaq · Pull Request #148 · chep/copilot-chat.el](https://github.com/chep/copilot-chat.el/pull/148)
author_association: COLLABORATOR
user: login: happyo
created_at: 2025-04-11T12:36:54Z
body: @lroolle 
In my use case, I use *copilot-chat-display* to ask temporary questions, so I need a better model like Claude3.7. Using the default-mode for everything doesn't seem to be a problem.

However, when I use *copilot-chat-insert-commit-message*, it becomes very slow, so there's a need to differentiate models between these two scenarios.

I would also suggest creating a new *copilot-chat-commit-model* specifically for *copilot-chat-insert-commit-message*, which would make it much more convenient to use.
author_association: NONE
user: login: lroolle
created_at: 2025-04-16T03:38:55Z
body: Hi @chep @ncaq @happyo, following your feedback, I've implemented a dedicated variable `copilot-chat-git-commit-model` and command `copilot-chat-set-git-commit-model` specifically for git commit message generation. 

Thx for the review!

author_association: CONTRIBUTOR