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