branch: elpa/gptel
commit 07715ecf099092be76f797493f515e51dbea203e
Author: Karthik Chikmagalur <[email protected]>
Commit: Karthik Chikmagalur <[email protected]>

    gptel: Record latest applied preset
    
    * gptel.el (gptel--preset): Use this (pre-existing) variable to
    record when a preset is applied.  Previously it was only used for
    preset indication in the transient menu.  It is now central to
    determining which preset was last applied in any buffer.
    
    (gptel--apply-preset): Set gptel--preset to the preset being
    applied, if it is provided as a symbol.
    
    (gptel--preset-mismatch-value): New function to check if a given
    value differs from a preset's value for key.  This will be used to
    simplify what metadata needs to be recorded when saving chat
    buffers.
    
    (gptel-with-preset): Add `gptel--preset' to the list of dynamic
    bindings, because we don't want to change its persistent value
    when using `gptel-with-preset' outside the scope of this macro.
    
    * gptel-transient.el (gptel--preset): Move to gptel.el
    
    (gptel--preset-mismatch-p): Mention new (similar) function
    `gptel--preset-mismatch-value'.
---
 gptel-transient.el | 11 ++++------
 gptel.el           | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 2 files changed, 66 insertions(+), 9 deletions(-)

diff --git a/gptel-transient.el b/gptel-transient.el
index 881d3478d57..476f80a3aa0 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -73,13 +73,11 @@ global value."
     (_ (kill-local-variable sym)
        (set sym value))))
 
-(defvar gptel--preset nil
-  "Name of last applied gptel preset.
-
-For internal use only.")
-
 (defun gptel--preset-mismatch-p (name)
-  "Check if gptel preset with NAME is in effect."
+  "Check if gptel preset with NAME is in effect.
+
+This is intended to be fast but imperfect.  See
+`gptel--preset-mismatch-value' for more granular checking."
   (let ((elm (or (gptel-get-preset name)
                  (gptel-get-preset (intern-soft name))))
         key val)
@@ -540,7 +538,6 @@ which see."
                                   ('t "buffer-locally")
                                   (_ "globally")))
                         gptel--known-presets nil t)))))
-  (gptel--set-with-scope 'gptel--preset name gptel--set-buffer-locally)
   (gptel--apply-preset
    name (lambda (sym val)
           (gptel--set-with-scope sym val gptel--set-buffer-locally)))
diff --git a/gptel.el b/gptel.el
index 3d0ae9aec21..1bd4b1b2003 100644
--- a/gptel.el
+++ b/gptel.el
@@ -330,6 +330,12 @@ configuration that might require a UI update.")
 (defvar-local gptel--bounds nil)
 (put 'gptel--bounds 'safe-local-variable #'always)
 
+(defvar gptel--preset nil
+  "Name of last applied gptel preset.
+
+For internal use only.")
+(put 'gptel--preset 'safe-local-variable #'symbolp)
+
 (defvar-local gptel--tool-names nil
   "Store to persist tool names to file across Emacs sessions.
 
@@ -2128,12 +2134,13 @@ SETTER is the function used to set the gptel options.  
It must accept
 two arguments, the symbol being set and the value to set it to.  It
 defaults to `set', and can be set to a different function to (for
 example) apply the preset buffer-locally."
+  (unless setter (setq setter #'set))
   (when (memq (type-of preset) '(string symbol))
     (let ((spec (or (gptel-get-preset preset)
                     (user-error "gptel preset \"%s\": Cannot find preset"
                                 preset))))
+      (funcall setter 'gptel--preset preset)
       (setq preset spec)))
-  (unless setter (setq setter #'set))
   (when-let* ((func (plist-get preset :pre))) (funcall func))
   (when-let* ((parents (plist-get preset :parents)))
     (mapc (lambda (parent) (gptel--apply-preset parent setter)) (ensure-list 
parents)))
@@ -2239,12 +2246,65 @@ NAME is the name of a preset, or a spec (plist) of the 
form
   (let ((syms (make-symbol "syms"))
         (binds (make-symbol "binds"))
         (bodyfun (make-symbol "body")))
-    `(let* ((,syms (gptel--preset-syms ,name))
+    ;; Let-bind symbols that we want to modify with the presets.  Also include
+    ;; `gptel--preset' in this list as we don't want to change its value 
outside
+    ;; of this macro's scope.
+    `(let* ((,syms (cons 'gptel--preset (gptel--preset-syms ,name)))
             (,bodyfun (lambda () (gptel--apply-preset ,name) ,@body))
             (,binds nil))
        (while ,syms (push (list (car ,syms) (pop ,syms)) ,binds))
        (eval (list 'let (nreverse ,binds) (list 'funcall (list 'quote 
,bodyfun)))))))
 
+(defun gptel--preset-mismatch-value (preset-spec key val)
+  "Determine if the value of KEY in PRESET-SPEC matches VAL.
+
+This is an imperfect check for whether the value corresponding to KEY (a
+keyword) in PRESET-SPEC (a plist) matches VAL.  This is required
+primarily to identify which gptel variable values have changed since
+PRESET-SPEC was applied, which is relevant when writing gptel metadata
+to a chat file.
+
+See also `gptel--preset-mismatch-p'."
+  ;; In all cases, assume a mismatch if the preset's value for KEY is a
+  ;; modify-list spec, such as (:append ...)
+  ;; Mismatches may not even be well-defined/determinable in these cases.
+  (or (not preset-spec)
+      (pcase key
+        ;; special cases
+        ((or :system :system-message)
+         (let ((system (plist-get preset-spec :system)))
+           (or (and (stringp system) (not (equal system val)))
+               (functionp system)
+               (and (consp system) (keywordp (car system)))
+               (and (consp system)
+                    (not (equal (car-safe (gptel--parse-directive system))
+                                val))))))
+        (:backend
+         (let ((backend (plist-get preset-spec :backend)))
+           (or (and (consp backend) (keywordp (car-safe backend)))
+               (not (equal (or (and (gptel-backend-p val) (gptel-backend-name 
val))
+                               val)
+                           (or (and (gptel-backend-p backend) 
(gptel-backend-name backend))
+                               backend))))))
+        ;; FIXME: We're assuming that val is a list of tool names, not tools
+        (:tools
+         (and-let* ((preset-tools (plist-get preset-spec :tools)))
+           (or (keywordp (car-safe preset-tools))
+               (cl-loop
+                for tool in preset-tools
+                for tool-name =
+                (or (and (stringp tool) tool)
+                    (ignore-errors (gptel-tool-name tool)))
+                if (not (member tool-name uniq-tool-names))
+                collect tool-name into uniq-tool-names
+                finally return
+                (not (equal (sort uniq-tool-names #'string-lessp)
+                            (sort (copy-sequence (ensure-list val)) 
#'string-lessp)))))))
+        ;; Generic case
+        (_ (let ((field-val (plist-get preset-spec key)))
+             (or (and (consp field-val) (keywordp (car field-val)))
+                 (not (equal field-val val))))))))
+
 ;;;; Presets in-buffer UI
 (defun gptel--transform-apply-preset (_fsm)
   "Apply a gptel preset to the buffer depending on the prompt.

Reply via email to