branch: elpa/gptel
commit d260da824f725620c87bacfac6ea956753b9c5a7
Author: Karthik Chikmagalur <[email protected]>
Commit: Karthik Chikmagalur <[email protected]>
gptel-integrations: Add vterm buffer support to gptel-send
Vterm buffers are always read-only in Emacs, which makes it
difficult to use with gptel-send. (A common use is to generate a
CLI command in place at the Vterm prompt.) Make gptel-send use
vterm-special insertion/deletion functions to work in Vterm
buffers, albeit without streaming support. (#1239)
Insertion works well, but support for deletion/in-place insertion
is still very flaky and dependent on the number of dynamic
terminal elements (such as "ghost text", changing text at the
right end of the line etc.) In-place substitution of prompts with
responses works best in a shell without any of these elements.
Committing this as a quick and approximate solution.
Support for Eat and term/ansi-term buffers is planned, depending
on user demand.
* gptel-integrations.el: Add helper functions for Vterm integration.
(gptel--vterm-delete): Try to delete a selected region (in
`vterm-copy-mode'), or backwards to the prompt.
(gptel--vterm-pre-insert): Handle insertion by collecting the
response in a temporary buffer and inserting into Vterm at the end.
* gptel-transient.el (gptel--suffix-send): Handle "in-place"
gptel requests in Vterm buffers.
* gptel.el (gptel--handle-pre-insert): Route response insertions
to the Vterm handler.
---
gptel-integrations.el | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++
gptel-transient.el | 7 +++++-
gptel.el | 36 +++++++++++++++++--------------
3 files changed, 86 insertions(+), 17 deletions(-)
diff --git a/gptel-integrations.el b/gptel-integrations.el
index 65b7de382b5..09b93a4464a 100644
--- a/gptel-integrations.el
+++ b/gptel-integrations.el
@@ -36,6 +36,66 @@
(require 'cl-lib)
(eval-when-compile (require 'transient))
+;;;;; Vterm integration
+;; Insertion and deletion is tricky in Vterm buffers. Try to ensure that
+;; gptel-send still works there. TODO: Insertion works, but insertion-in-place
+;; is flaky and fails depending on how well Vterm's prompt tracking works, as
+;; well as on the presence of "virtual text" in the prompt.
+(declare-function vterm-copy-mode "vterm")
+(declare-function vterm-delete-region "vterm")
+(declare-function vterm-goto-char "vterm")
+(declare-function vterm-reset-cursor-point "vterm")
+(declare-function vterm-cursor-in-command-buffer-p "vterm")
+(declare-function vterm-send-key "vterm")
+(declare-function vterm-insert "vterm")
+(defvar vterm-copy-mode)
+
+(defun gptel--vterm-delete ()
+ "Try to delete the region or Vterm prompt text.
+
+Intended for use in Vterm buffers with the \"respond-in-place\" option
+of `gptel-send'."
+ (if (use-region-p)
+ (let ((beg (region-beginning)) ; Clear region
+ (end (region-end)))
+ (vterm-copy-mode -1)
+ (condition-case nil
+ ;; Preferred solution, fails if the prompt is part of region
+ (vterm-delete-region beg end)
+ (buffer-read-only
+ (when (vterm-goto-char end) ;HACK Try to clear characters one by
one
+ (let ((prev-pt (1- end)))
+ (while (and (>= (vterm-reset-cursor-point) beg)
+ (/= (point) prev-pt)
+ (vterm-cursor-in-command-buffer-p))
+ (setq prev-pt (point))
+ (vterm-send-key "<backspace>" nil t nil t)))))))
+ (let ((prev-pt 0)) ; Clear to prompt
+ (while (and (/= (vterm-reset-cursor-point) prev-pt)
+ (vterm-cursor-in-command-buffer-p))
+ (setq prev-pt (point))
+ (vterm-send-key "<backspace>" nil t nil t)))))
+
+(defun gptel--vterm-pre-insert (info)
+ "Set up insertion into Vterm buffers for `gptel-send'.
+
+INFO is the query information for the active request."
+ (let ((start-marker (plist-get info :position))
+ (hold-buffer (gptel--temp-buffer " *gptel-vterm-redirect*")))
+ (plist-put info :vterm-marker (copy-marker start-marker t))
+ (with-current-buffer hold-buffer
+ (move-marker start-marker (point-min) hold-buffer)
+ ;; We collect text elsewhere and copy it into the Vterm buffer at the end
+ (add-hook 'gptel-post-response-functions
+ (lambda (beg end)
+ (let ((response (buffer-substring-no-properties beg end)))
+ (with-current-buffer (plist-get info :buffer)
+ (goto-char (plist-get info :vterm-marker))
+ (when vterm-copy-mode (vterm-copy-mode -1))
+ (vterm-insert response)))
+ (kill-buffer (current-buffer)))
+ 90 t))))
+
;;;; MCP integration - requires the mcp package
(declare-function mcp-hub-get-all-tool "mcp-hub")
(declare-function mcp-hub-get-servers "mcp-hub")
diff --git a/gptel-transient.el b/gptel-transient.el
index 476f80a3aa0..183350e59e5 100644
--- a/gptel-transient.el
+++ b/gptel-transient.el
@@ -32,6 +32,7 @@
(declare-function ediff-regions-internal "ediff")
(declare-function ediff-make-cloned-buffer "ediff-utils")
(declare-function org-escape-code-in-string "org-src")
+(declare-function gptel--vterm-delete "gptel-integrations")
;; * Helper functions and vars
@@ -1767,7 +1768,11 @@ This sets the variable `gptel-include-tool-results',
which see."
;; text is killed below.
(when in-place
(if (or buffer-read-only (get-char-property (point) 'read-only))
- (message "Not replacing prompt: region is read-only")
+ (cond
+ ((derived-mode-p 'vterm-mode)
+ (require 'gptel-integrations)
+ (gptel--vterm-delete))
+ (t (message "Not replacing prompt: region is read-only")))
(let ((beg (if (use-region-p)
(region-beginning)
(max (previous-single-property-change
diff --git a/gptel.el b/gptel.el
index 54c6d8d4c7d..4555652fa2f 100644
--- a/gptel.el
+++ b/gptel.el
@@ -196,6 +196,7 @@
(declare-function gptel-menu "gptel-transient")
(declare-function gptel-system-prompt "gptel-transient")
(declare-function gptel-tools "gptel-transient")
+(declare-function gptel--vterm-pre-insert "gptel-integrations")
(declare-function pulse-momentary-highlight-region "pulse")
(declare-function ediff-make-cloned-buffer "ediff-util")
@@ -1191,22 +1192,25 @@ Handle read-only buffers and run pre-response hooks
(but only if
the request succeeded)."
(let* ((info (gptel-fsm-info fsm))
(start-marker (plist-get info :position)))
- (when (and
- (memq (plist-get info :callback)
- '(gptel--insert-response gptel-curl--stream-insert-response))
- (with-current-buffer (plist-get info :buffer)
- (or buffer-read-only
- (get-char-property start-marker 'read-only))))
- (message "Buffer is read only, displaying reply in buffer \"*LLM
response*\"")
- (display-buffer
- (with-current-buffer (get-buffer-create "*LLM response*")
- (visual-line-mode 1)
- (goto-char (point-max))
- (move-marker start-marker (point) (current-buffer))
- (current-buffer))
- '((display-buffer-reuse-window
- display-buffer-pop-up-window)
- (reusable-frames . visible))))
+ (when (memq (plist-get info :callback)
+ '(gptel--insert-response gptel-curl--stream-insert-response))
+ (with-current-buffer (plist-get info :buffer)
+ (when (or buffer-read-only (get-char-property start-marker 'read-only))
+ (cond
+ ((derived-mode-p 'vterm-mode)
+ (require 'gptel-integrations)
+ (gptel--vterm-pre-insert info))
+ (t
+ (message "Buffer is read only, displaying reply in buffer \"*LLM
response*\"")
+ (display-buffer
+ (with-current-buffer (get-buffer-create "*LLM response*")
+ (visual-line-mode 1)
+ (goto-char (point-max))
+ (move-marker start-marker (point) (current-buffer))
+ (current-buffer))
+ '((display-buffer-reuse-window
+ display-buffer-pop-up-window)
+ (reusable-frames . visible))))))))
(with-current-buffer (marker-buffer start-marker)
(when (plist-get info :stream)
(gptel--update-status " Typing..." 'success))