gptel: Add response regeneration, history and ediff

* gptel.el (gptel--attach-response-history, gptel--ediff,
gptel--next-variant, gptel--previous-variant,
gptel--mark-response):

Add `gptel--attach-response-history` -- this can be used to add
text properties to the next gptel response in the buffer.  This
is (currently) useful for tracking changes when the response
overwrites existing text.

The next three commands -- `gptel--ediff`,
`gptel--previous-variant`, `gptel--next-variant` -- provide
facilities for manipulating a gptel response at point when there
is history.  `gptel--mark-response` marks the response at point.
These are considered internal functions for now and can be
accessed from the transient menu, where they work together with
`gptel--regenerate`.

The input arguments to these commands are expected to change to
support copilot-style functionality in the near future.

* gptel-transient.el (gptel-menu, gptel--suffix-send,
gptel--regenerate):

Change the transient menu layout to be more compact (with a newly
added column.)  When overwriting the prompt with a response, save
the prompt to the gptel response's history.  Add
`gptel--regenerate` to regenerate a response.  This is accessible
from the transient menu when the point is inside response text.
This commit is contained in:
Karthik Chikmagalur 2024-02-04 21:09:53 -08:00
parent 49cfc78378
commit bf994c0765
2 changed files with 159 additions and 7 deletions

View file

@ -126,11 +126,12 @@ which see."
(gptel--infix-max-tokens) (gptel--infix-max-tokens)
(gptel--infix-num-messages-to-send) (gptel--infix-num-messages-to-send)
(gptel--infix-temperature)] (gptel--infix-temperature)]
["Prompt:" ["Prompt from"
("p" "From minibuffer instead" "p") ("p" "Minibuffer instead" "p")
("y" "From kill-ring instead" "y") ("y" "Kill-ring instead" "y")
("i" "Replace/Delete prompt" "i") ""
"Response to:" ("i" "Replace/Delete prompt" "i")]
["Response to"
("m" "Minibuffer instead" "m") ("m" "Minibuffer instead" "m")
("g" "gptel session" "g" ("g" "gptel session" "g"
:class transient-option :class transient-option
@ -151,7 +152,10 @@ which see."
:reader :reader
(lambda (prompt _ _history) (lambda (prompt _ _history)
(read-buffer prompt (buffer-name (other-buffer)) nil))) (read-buffer prompt (buffer-name (other-buffer)) nil)))
("k" "Kill-ring" "k")] ("k" "Kill-ring" "k")]]
[["Send"
(gptel--suffix-send)
("M-RET" "Regenerate" gptel--regenerate :if gptel--in-response-p)]
[:description gptel--refactor-or-rewrite [:description gptel--refactor-or-rewrite
:if use-region-p :if use-region-p
("r" ("r"
@ -161,7 +165,16 @@ which see."
(lambda () (if (derived-mode-p 'prog-mode) (lambda () (if (derived-mode-p 'prog-mode)
"Refactor" "Rewrite")) "Refactor" "Rewrite"))
gptel-rewrite-menu)] gptel-rewrite-menu)]
["Send" (gptel--suffix-send)]] ["Tweak Response" :if gptel--in-response-p :pad-keys t
("SPC" "Mark" gptel--mark-response)
("P" "Previous variant" gptel--previous-variant
:if gptel--at-response-history-p
:transient t)
("N" "Next variant" gptel--previous-variant
:if gptel--at-response-history-p
:transient t)
("E" "Ediff previous" gptel--ediff
:if gptel--at-response-history-p)]]
(interactive) (interactive)
(gptel--sanitize-model) (gptel--sanitize-model)
(transient-setup 'gptel-menu)) (transient-setup 'gptel-menu))
@ -510,6 +523,10 @@ responses."
t)) t))
(point)))) (point))))
(end (if (use-region-p) (region-end) (point)))) (end (if (use-region-p) (region-end) (point))))
(unless output-to-other-buffer-p
;; store the killed text in gptel-history
(gptel--attach-response-history
(list (buffer-substring-no-properties beg end))))
(kill-region beg end))) (kill-region beg end)))
(gptel-request (gptel-request
@ -527,6 +544,30 @@ responses."
display-buffer-pop-up-window) display-buffer-pop-up-window)
(reusable-frames . visible)))))) (reusable-frames . visible))))))
;; ** Suffix to regenerate response
(defun gptel--regenerate ()
"Regenerate gptel response at point."
(interactive)
(when (gptel--in-response-p)
(pcase-let* ((`(,beg . ,end) (gptel--get-bounds))
(history (get-char-property (point) 'gptel-history))
(prev-responses (cons (buffer-substring-no-properties beg end)
history)))
(when gptel-mode ;Remove prefix/suffix
(save-excursion
(goto-char beg)
(when (looking-back (concat "\n+" (regexp-quote (gptel-response-prefix-string)))
(point-min) 'greedy)
(setq beg (match-beginning 0)))
(goto-char end)
(when (looking-at
(concat "\n+" (regexp-quote (gptel-prompt-prefix-string))))
(setq end (match-end 0)))))
(delete-region beg end)
(gptel--attach-response-history prev-responses)
(call-interactively #'gptel--suffix-send))))
;; ** Set system message ;; ** Set system message
(defun gptel--read-crowdsourced-prompt () (defun gptel--read-crowdsourced-prompt ()
"Pick a crowdsourced system prompt for gptel. "Pick a crowdsourced system prompt for gptel.

111
gptel.el
View file

@ -1352,5 +1352,116 @@ text stream."
(prog1 (buffer-substring (point) (point-max)) (prog1 (buffer-substring (point) (point-max))
(set-marker start-pt (point-max))))))))) (set-marker start-pt (point-max)))))))))
;; Response tweaking commands
(defun gptel--attach-response-history (history &optional buf)
"Attach HISTORY to the next gptel response in buffer BUF.
HISTORY is a list of strings typically containing text replaced
by gptel. BUF is the current buffer if not specified.
This is used to maintain variants of prompts or responses to diff
against if required."
(with-current-buffer (or buf (current-buffer))
(letrec ((gptel--attach-after
(lambda (b e)
(put-text-property b e 'gptel-history
(append (ensure-list history)
(get-char-property (1- e) 'gptel-history)))
(remove-hook 'gptel-post-response-functions
gptel--attach-after 'local))))
(add-hook 'gptel-post-response-functions gptel--attach-after
nil 'local))))
(defun gptel--ediff (&optional arg bounds-func)
"Ediff response at point against previous gptel responses.
If prefix ARG is non-nil, select the previous response to ediff
against interactively.
If specified, use BOUNDS-FUNC to compute the bounds of the
response at point. This can be used to include additional
context for the ediff session."
(interactive "P")
(when (gptel--at-response-history-p)
(pcase-let* ((`(,beg . ,end) (funcall (or bounds-func #'gptel--get-bounds)))
(prev-response
(if arg
(completing-read "Choose response variant to diff against: "
(get-char-property (point) 'gptel-history)
nil t)
(car-safe (get-char-property (point) 'gptel-history))))
(buffer-mode major-mode)
(bufname (buffer-name))
(`(,new-buf ,new-beg ,new-end)
(with-current-buffer
(get-buffer-create (concat bufname "-PREVIOUS-*"))
(let ((inhibit-read-only t))
(erase-buffer)
(delay-mode-hooks (funcall buffer-mode))
(insert prev-response)
(goto-char (point-min))
(list (current-buffer) (point-min) (point-max))))))
(unless prev-response (user-error "gptel response is additive: no changes to ediff"))
(require 'ediff)
(letrec ((cwc (current-window-configuration))
(gptel--ediff-restore
(lambda ()
(when (window-configuration-p cwc)
(set-window-configuration cwc))
(kill-buffer (get-buffer (concat bufname "-PREVIOUS-*")))
(kill-buffer (get-buffer (concat bufname "-CURRENT-*")))
(remove-hook 'ediff-quit-hook gptel--ediff-restore))))
(add-hook 'ediff-quit-hook gptel--ediff-restore)
(apply
#'ediff-regions-internal
(get-buffer (ediff-make-cloned-buffer (current-buffer) "-CURRENT-*"))
beg end new-buf new-beg new-end
nil
(list 'ediff-regions-wordwise 'word-wise nil)
;; (if (transient-arg-value "-w" args)
;; (list 'ediff-regions-wordwise 'word-wise nil)
;; (list 'ediff-regions-linewise nil nil))
)))))
(defun gptel--mark-response ()
"Mark gptel response at point, if any."
(interactive)
(unless (gptel--in-response-p) (user-error "No gptel response at point"))
(pcase-let* ((`(,beg . ,end) (gptel--get-bounds)))
(goto-char beg) (push-mark) (goto-char end) (activate-mark)))
(defun gptel--previous-variant (&optional arg)
"Switch to previous gptel-response at this point, if it exists."
(interactive "p")
(pcase-let* ((`(,beg . ,end) (gptel--get-bounds))
(history (get-char-property (point) 'gptel-history))
(alt-response (car-safe history))
(offset))
(unless (and history alt-response)
(user-error "No variant responses available"))
(if (> arg 0)
(setq history (append (cdr history)
(list (buffer-substring-no-properties beg end))))
(setq
alt-response (car (last history))
history (cons (buffer-substring-no-properties beg end)
(nbutlast history))))
(add-text-properties
0 (length alt-response)
`(gptel response rear-nonsticky t gptel-history ,history)
alt-response)
(setq offset (min (- (point) beg) (1- (length alt-response))))
(delete-region beg end)
(insert alt-response)
(goto-char (+ beg offset))
(pulse-momentary-highlight-region beg (+ beg (length alt-response)))))
(defun gptel--next-variant (&optional arg)
"Switch to next gptel-response at this point, if it exists."
(interactive "p")
(gptel--previous-variant (- arg)))
(provide 'gptel) (provide 'gptel)
;;; gptel.el ends here ;;; gptel.el ends here