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

    gptel: Add minor mode to highlight LLM responses
    
    New, oft-requested feature `gptel-highlight-mode' to highlight
    LLM response text regions (as well as tool calls and ignored
    regions).
    
    * gptel.el (gptel-highlight-fringe): Bitmap for fringe
    highlighting.
    
    (gptel-highlight-mode): New minor-mode to highlight gptel LLM
    responses.  Works in any buffer in Emacs.
    
    (gptel-highlight-methods): New user option to configure how
    highlighting should be performed.  The default is to use the left
    margin, since this works on TTY also.  Other options are a fringe
    bitmap and a response face.  The face, while comprehensive, can
    obscure other Org/Markdown formatting and is thus not the default.
    
    (gptel-response-fringe-highlight, gptel-response-highlight): Faces
    for highlighting.
    
    NOTE: The face names are subject to change, since it is not clear
    yet if it might be better to suffix faces with "-face".  gptel's
    approach here is currently inconsistent and will be fixed
    eventually.
    
    (gptel-highlight--update, gptel-highlight--decorate,
    gptel-highlight--fringe-prefix, gptel-highlight--margin-prefix):
    Implementation of `gptel-highlight-mode'.
    
    * README.org (Usage, FAQ): Mention `gptel-highlight-mode'.
    * NEWS (New features and UI changes): Mention change.
---
 NEWS       |   8 ++++
 README.org |  10 ++++-
 gptel.el   | 146 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 161 insertions(+), 3 deletions(-)

diff --git a/NEWS b/NEWS
index f65fc309af1..63383bec4de 100644
--- a/NEWS
+++ b/NEWS
@@ -55,6 +55,14 @@
 
 ** New features and UI changes
 
+- New minor-mode ~gptel-highlight-mode~ to highlight LLM responses and
+  more.  An oft-requested feature, gptel can now highlight responses by
+  decorating the (left) margin or fringe, and apply a face to the
+  response region.  To use it, just turn on ~gptel-highlight-mode~ in
+  any buffer (and not just dedicated chat buffers).  You can customize
+  the type of decoration performed via ~gptel-highlight-methods~, which
+  see.
+
 - Link annotations: When ~gptel-track-media~ is enabled in gptel chat
   buffers, gptel follows (Markdown/Org) links to files in the prompt and
   includes these files with queries.  However, it was not clear if a
diff --git a/README.org b/README.org
index e82029e1157..69a1a8253bf 100644
--- a/README.org
+++ b/README.org
@@ -1252,6 +1252,8 @@ gptel provides a few powerful, general purpose and 
flexible commands.  You can d
 
 #+html: <img 
src="https://github.com/karthink/gptel/assets/8607532/3562a6e2-7a5c-4f7e-8e57-bf3c11589c73";
 align="center" alt="Image showing gptel's menu with some of the available 
query options.">
 
+You can use =gptel-highlight-mode= to highlight LLM responses in different 
ways.
+
 You can also define a "preset" bundle of options that are applied together, 
see [[#option-presets][Option presets]] below.
 
 *** In a dedicated chat buffer:
@@ -1270,6 +1272,8 @@ That's it. You can go back and edit previous prompts and 
responses if you want.
 
 The default mode is =markdown-mode= if available, else =text-mode=.  You can 
set =gptel-default-mode= to =org-mode= if desired.
 
+You can use =gptel-highlight-mode= to highlight LLM responses in different 
ways.
+
 You can also define a "preset" bundle of options that are applied together, 
see [[#option-presets][Option presets]] below.
 
 #+html: <details><summary>
@@ -1588,9 +1592,11 @@ You can also call =gptel-end-of-response= as a command 
at any time.
 **** I want to change the formatting of the prompt and LLM response
 #+html: </summary>
 
-For dedicated chat buffers: customize =gptel-prompt-prefix-alist= and 
=gptel-response-prefix-alist=.  You can set a different pair for each 
major-mode.
+Anywhere in Emacs: Turn on =gptel-highlight-mode=.  See its documentation for 
customization options.
+
+In dedicated chat buffers: you can additionally customize 
=gptel-prompt-prefix-alist= and =gptel-response-prefix-alist=, which are 
prefixes inserted before the prompt and response.  You can set a different pair 
for each major-mode.
 
-Anywhere in Emacs: Use =gptel-pre-response-hook= and 
=gptel-post-response-functions=, which see.
+For more custom formatting: Use =gptel-pre-response-hook= and 
=gptel-post-response-functions=, which see.
 
 #+html: </details>
 #+html: <details><summary>
diff --git a/gptel.el b/gptel.el
index 685d7804f85..485b576b306 100644
--- a/gptel.el
+++ b/gptel.el
@@ -651,7 +651,7 @@ file."
           (add-file-local-variable 'gptel--bounds 
(gptel--get-buffer-bounds)))))))
 
 
-;;; Minor mode and UI
+;;; Minor modes and UI
 
 ;; NOTE: It's not clear that this is the best strategy:
 (cl-pushnew '(gptel . t) (default-value 'text-property-default-nonsticky)
@@ -855,6 +855,150 @@ Search between BEG and END."
     (force-mode-line-update)))
 
 
+;;;; gptel-highlight-mode
+
+(defcustom gptel-highlight-methods '(margin)
+  "Types of LLM response highlighting used by `gptel-highlight-mode'.
+
+This must be a list of symbols denoting types of highlighting for LLM 
responses:
+- face: highlight LLM responses using face `gptel-response-highlight'.
+- fringe: highlight using a (left) fringe marker.
+- margin: highlight in the (left) display margin.
+
+margin and fringe markings are mutually exclusive, and use the
+`gptel-response-fringe-highlight' face."
+  :type '(set (const :tag "Fringe marker" fringe)
+              (const :tag "Face highlighting" face)
+              (const :tag "Margin indicator" margin))
+  :group 'gptel)
+
+(defface gptel-response-highlight
+  '((((background light) (min-colors 88)) :background "linen" :extend t)
+    (((background dark)  (min-colors 88)) :background "gray14" :extend t)
+    (t :inherit mode-line))
+  "Face used to highlight LLM responses when using `gptel-highlight-mode'.
+
+To enable this face for responses, `gptel-highlight-methods' must be set."
+  :group 'gptel)
+
+(defface gptel-response-fringe-highlight
+  '((t :inherit outline-1 :height reset))
+  "LLM response fringe/margin face when using `gptel-highlight-mode'.
+
+To enable response highlights in the fringe, `gptel-highlight-methods'
+must be set."
+  :group 'gptel)
+
+(define-fringe-bitmap 'gptel-highlight-fringe
+  (make-vector 28 #b01100000)
+  nil nil 'center)
+
+;; Common options for margin indicator:
+;; BOX DRAWINGS LIGHT VERTICAL  0x002502
+;; LEFT ONE QUARTER BLOCK       0x00258E
+;; LEFT THREE EIGHTHS BLOCK     0x00258D
+;; BOX DRAWINGS HEAVY VERTICAL  0x002503
+;; VERTICAL ONE EIGHTH BLOCK-2  0x01FB70
+
+(defun gptel-highlight--margin-prefix (type)
+  "Create margin prefix string for TYPE.
+
+Supported TYPEs are response, ignore and tool calls."
+  (propertize ">" 'display
+              `( (margin left-margin)
+                 ,(propertize "▎" 'face
+                              (pcase type
+                                ('response 'gptel-response-fringe-highlight)
+                                ('ignore 'shadow)
+                                (`(tool . ,_) 'shadow))))))
+
+(defun gptel-highlight--fringe-prefix (type)
+  "Create fringe prefix string for TYPE.
+
+Supported TYPEs are response, ignore and tool calls."
+  (propertize ">" 'display
+              `( left-fringe gptel-highlight-fringe
+                 ,(pcase type
+                    ('response 'gptel-response-fringe-highlight)
+                    ('ignore 'shadow)
+                    (`(tool . ,_) 'shadow)))))
+
+(defun gptel-highlight--decorate (ov &optional val)
+  "Decorate gptel indicator overlay OV whose type is VAL."
+  (overlay-put ov 'evaporate t)
+  (overlay-put ov 'gptel-highlight t)
+  (when (memq 'face gptel-highlight-methods)
+    (overlay-put ov 'font-lock-face
+                 (pcase val
+                   ('response 'gptel-response-highlight)
+                   ('ignore 'shadow)
+                   (`(tool . ,_) 'shadow))))
+  (when-let* ((prefix
+               (cond ((memq 'margin gptel-highlight-methods)
+                      (gptel-highlight--margin-prefix (or val 'response)))
+                     ((memq 'fringe gptel-highlight-methods)
+                      (gptel-highlight--fringe-prefix (or val 'response))))))
+    (overlay-put ov 'line-prefix prefix)
+    (overlay-put ov 'wrap-prefix prefix)))
+
+(defun gptel-highlight--update (beg end)
+  "JIT-lock function: mark gptel response/reasoning regions.
+
+BEG and END delimit the region to refresh."
+  (save-excursion                ;Scan across region for the gptel text 
property
+    (let ((prev-pt (goto-char end)))
+      (while (and (goto-char (previous-single-property-change
+                              (point) 'gptel nil beg))
+                  (/= (point) prev-pt))
+        (pcase (get-char-property (point) 'gptel)
+          ((and (or 'response 'ignore `(tool . ,_)) val)
+           (if-let* ((ov (or (cdr-safe (get-char-property-and-overlay
+                                        (point) 'gptel-highlight))
+                             (cdr-safe (get-char-property-and-overlay
+                                        prev-pt 'gptel-highlight))))
+                     (from (overlay-start ov)) (to (overlay-end ov)))
+               (unless (<= from (point) prev-pt to)
+                 (move-overlay ov (min from (point)) (max to prev-pt)))
+             (gptel-highlight--decorate ;Or make new overlay covering just 
region
+              (make-overlay (point) prev-pt nil t) val)))
+          ('nil                     ;If there's an overlay, we need to split 
it.
+           (when-let* ((ov (cdr-safe (get-char-property-and-overlay
+                                      (point) 'gptel-highlight)))
+                       (from (overlay-start ov)) (to (overlay-end ov)))
+             (move-overlay ov from (point)) ;Move overlay to left side
+             (gptel-highlight--decorate     ;Make a new one on the right
+              (make-overlay prev-pt to nil t)
+              (get-char-property prev-pt 'gptel)))))
+        (setq prev-pt (point)))))
+  `(jit-lock-bounds ,beg . ,end))
+
+(define-minor-mode gptel-highlight-mode
+  "Visually highlight LLM respones regions.
+
+Highlighting is via fringe or margin markers, and optionally a response
+face.  See `gptel-highlight-methods' for highlighting methods, and
+`gptel-response-highlight' and `gptel-response-fringe-highlight' for the
+faces.
+
+This minor mode can be used anywhere in Emacs, and not just gptel chat
+buffers."
+  :lighter nil
+  :global nil
+  (cond
+   (gptel-highlight-mode
+    (when (memq 'margin gptel-highlight-methods)
+      (setq left-margin-width (1+ left-margin-width))
+      (set-window-buffer (selected-window) (current-buffer)))
+    (jit-lock-register #'gptel-highlight--update)
+    (gptel-highlight--update (point-min) (point-max)))
+   (t (when (memq 'margin gptel-highlight-methods)
+        (setq left-margin-width (max (1- left-margin-width) 0))
+        (set-window-buffer (selected-window) (current-buffer)))
+      (jit-lock-unregister #'gptel-highlight--update)
+      (without-restriction
+        (remove-overlays nil nil 'gptel-highlight t)))))
+
+
 ;;; State machine additions for `gptel-send'.
 
 (defvar gptel-send--handlers

Reply via email to