From bf994c0765f69cc2ce467bc5f023c99be608aee5 Mon Sep 17 00:00:00 2001 From: Karthik Chikmagalur Date: Sun, 4 Feb 2024 21:09:53 -0800 Subject: [PATCH] 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. --- gptel-transient.el | 55 +++++++++++++++++++--- gptel.el | 111 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 7 deletions(-) diff --git a/gptel-transient.el b/gptel-transient.el index 4e998c1..00ae53b 100644 --- a/gptel-transient.el +++ b/gptel-transient.el @@ -126,11 +126,12 @@ which see." (gptel--infix-max-tokens) (gptel--infix-num-messages-to-send) (gptel--infix-temperature)] - ["Prompt:" - ("p" "From minibuffer instead" "p") - ("y" "From kill-ring instead" "y") - ("i" "Replace/Delete prompt" "i") - "Response to:" + ["Prompt from" + ("p" "Minibuffer instead" "p") + ("y" "Kill-ring instead" "y") + "" + ("i" "Replace/Delete prompt" "i")] + ["Response to" ("m" "Minibuffer instead" "m") ("g" "gptel session" "g" :class transient-option @@ -151,7 +152,10 @@ which see." :reader (lambda (prompt _ _history) (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 :if use-region-p ("r" @@ -161,7 +165,16 @@ which see." (lambda () (if (derived-mode-p 'prog-mode) "Refactor" "Rewrite")) 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) (gptel--sanitize-model) (transient-setup 'gptel-menu)) @@ -510,6 +523,10 @@ responses." t)) (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))) (gptel-request @@ -527,6 +544,30 @@ responses." display-buffer-pop-up-window) (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 (defun gptel--read-crowdsourced-prompt () "Pick a crowdsourced system prompt for gptel. diff --git a/gptel.el b/gptel.el index 3add09e..4d3256b 100644 --- a/gptel.el +++ b/gptel.el @@ -1352,5 +1352,116 @@ text stream." (prog1 (buffer-substring (point) (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) ;;; gptel.el ends here