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