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))

Reply via email to