Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions ai-code-prompt-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
(defvar ai-code-discussion-auto-follow-up-enabled)
(defvar ai-code-discussion-auto-follow-up-suffix)
(defvar ai-code-use-prompt-suffix)
(defvar ai-code-backends-infra--session-terminal-backend nil
"Buffer-local terminal backend symbol for an AI session buffer, or nil.
This is set by `ai-code-backends-infra.el' for terminal-managed sessions
such as `vterm' and `eat'. A nil value means the buffer is not managed by
the terminal backend infrastructure.")

(declare-function yas-load-directory "yasnippet" (dir))
(declare-function yas-minor-mode "yasnippet")
Expand All @@ -35,6 +40,11 @@
(declare-function ai-code--choose-symbol-from-file "ai-code-input" (file))
(declare-function ai-code-read-string "ai-code-input" (prompt &optional initial-input candidate-list))
(declare-function ai-code-current-backend-label "ai-code-backends" ())
(declare-function ai-code-backends-infra--session-buffer-p "ai-code-backends-infra" (buffer))
(declare-function ai-code-backends-infra--session-buffer-matches-directory-p "ai-code-backends-infra" (buffer directory))
(declare-function ai-code-backends-infra--terminal-send-string "ai-code-backends-infra" (string))
(declare-function ai-code-backends-infra--terminal-send-return "ai-code-backends-infra" ())
(declare-function ai-code-backends-infra--display-buffer-in-side-window "ai-code-backends-infra" (buffer))

(defcustom ai-code-prompt-preprocess-filepaths t
"When non-nil, preprocess the prompt to replace file paths.
Expand Down Expand Up @@ -176,10 +186,70 @@ that should be recorded in the prompt history file."
(ai-code--insert-backend-label-drawer)
(ai-code--format-and-insert-prompt stored-prompt-text))

(defun ai-code--find-visible-session-buffer ()
"Return a visible terminal-managed AI session buffer in the current frame."
(cl-some
(lambda (win)
(let ((buf (window-buffer win)))
(when (and (buffer-live-p buf)
(ai-code-backends-infra--session-buffer-p buf)
(buffer-local-value
'ai-code-backends-infra--session-terminal-backend buf))
buf)))
(window-list nil 'no-minibuffer)))

(defun ai-code--find-project-session-buffers ()
"Return terminal-managed AI session buffers associated with the current project."
(when-let ((git-root (ai-code--git-root)))
(cl-remove-if-not
(lambda (buf)
(and (ai-code-backends-infra--session-buffer-p buf)
(buffer-local-value
'ai-code-backends-infra--session-terminal-backend buf)
(ai-code-backends-infra--session-buffer-matches-directory-p buf git-root)))
(buffer-list))))

(defun ai-code--prompt-choose-target-session ()
"Choose AI session buffer to send prompt to.
Return session buffer for direct routing, or nil to use default backend dispatch."
(when-let ((visible-session (ai-code--find-visible-session-buffer)))
(let* ((project-sessions (ai-code--find-project-session-buffers))
(visible-is-project-session (memq visible-session project-sessions))
(competing-sessions
(cl-remove visible-session project-sessions)))
(cond
(visible-is-project-session nil)
((null competing-sessions)
visible-session)
(t
(let* ((choice-alist (mapcar (lambda (buf)
(cons (buffer-name buf) buf))
(cons visible-session competing-sessions)))
(selection (completing-read
"Multiple AI sessions available. Send to: "
(mapcar #'car choice-alist)
nil t nil nil (buffer-name visible-session))))
(cdr (assoc selection choice-alist))))))))
Comment on lines +212 to +232

(defun ai-code--send-prompt-to-session-buffer (prompt buffer)
"Send PROMPT directly to session BUFFER and display it."
(with-current-buffer buffer
(ai-code-backends-infra--terminal-send-string prompt)
(sit-for 0.5)
(ai-code-backends-infra--terminal-send-return))
(if-let ((window (get-buffer-window buffer)))
(select-window window)
(ai-code-backends-infra--display-buffer-in-side-window buffer)))
Comment on lines +234 to +242

(defun ai-code--send-prompt (full-prompt)
"Send FULL-PROMPT to AI."
(ai-code-cli-send-command full-prompt)
(ai-code-cli-switch-to-buffer))
"Send FULL-PROMPT to AI.
When a visible AI session buffer is detected in the current frame,
send the prompt directly to it instead of going through the default
backend dispatch."
(if-let ((target (ai-code--prompt-choose-target-session)))
(ai-code--send-prompt-to-session-buffer full-prompt target)
(ai-code-cli-send-command full-prompt)
(ai-code-cli-switch-to-buffer)))

(defun ai-code--write-prompt-to-file-and-send (prompt-text)
"Write PROMPT-TEXT to the AI prompt file."
Expand Down
164 changes: 164 additions & 0 deletions test/test_ai-code-prompt-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -1474,5 +1474,169 @@ and ensures everything is cleaned up afterward."
(should (string-match-p (regexp-quote ":AGENT: gemini") content))
(should (string-match-p (regexp-quote ":END:") content))))))

;;; Tests for visible session routing in ai-code--send-prompt

(ert-deftest ai-code-test-find-visible-session-buffer-returns-session ()
"Find a session buffer in visible windows."
(let ((session-buf (get-buffer-create "*claude[test-project]*")))
(unwind-protect
(with-current-buffer session-buf
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
(cl-letf (((symbol-function 'window-list)
(lambda (&optional _frame _no-minibuf)
'(win1 win2)))
((symbol-function 'window-buffer)
(lambda (win)
(if (eq win 'win1) (get-buffer "*scratch*") session-buf))))
(should (eq (ai-code--find-visible-session-buffer) session-buf))))
(kill-buffer session-buf))))

(ert-deftest ai-code-test-find-visible-session-buffer-nil-when-no-sessions ()
"Return nil when no session buffers are visible."
(cl-letf (((symbol-function 'window-list)
(lambda (&optional _frame _no-minibuf) '(win1)))
((symbol-function 'window-buffer)
(lambda (_win) (get-buffer "*scratch*"))))
(should-not (ai-code--find-visible-session-buffer))))

(ert-deftest ai-code-test-find-visible-session-buffer-ignores-non-terminal-sessions ()
"Ignore visible session-like buffers that are not terminal managed."
(let ((session-buf (get-buffer-create "*claude[test-project]*")))
(unwind-protect
(cl-letf (((symbol-function 'window-list)
(lambda (&optional _frame _no-minibuf) '(win1)))
((symbol-function 'window-buffer)
(lambda (_win) session-buf)))
(should-not (ai-code--find-visible-session-buffer)))
(kill-buffer session-buf))))

(ert-deftest ai-code-test-find-project-session-buffers-finds-matching ()
"Find session buffers matching the current project directory."
(ai-code-with-test-repo
(let ((session-buf (get-buffer-create "*claude[test-repo]*")))
(unwind-protect
(with-current-buffer session-buf
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
(cl-letf (((symbol-function 'ai-code-backends-infra--session-buffer-matches-directory-p)
(lambda (_buf _dir) (eq _buf session-buf))))
(should (memq session-buf (ai-code--find-project-session-buffers)))))
(kill-buffer session-buf)))))

(ert-deftest ai-code-test-find-project-session-buffers-excludes-other-projects ()
"Exclude session buffers for other projects."
(ai-code-with-test-repo
(let ((other-buf (get-buffer-create "*claude[other-project]*")))
(unwind-protect
(with-current-buffer other-buf
(setq-local ai-code-backends-infra--session-terminal-backend 'vterm)
(cl-letf (((symbol-function 'ai-code-backends-infra--session-buffer-matches-directory-p)
(lambda (_buf _dir) nil)))
(should-not (memq other-buf (ai-code--find-project-session-buffers)))))
(kill-buffer other-buf)))))

(ert-deftest ai-code-test-find-project-session-buffers-excludes-non-terminal-sessions ()
"Exclude project sessions that are not terminal managed."
(ai-code-with-test-repo
(let ((session-buf (get-buffer-create "*claude[test-repo]*")))
(unwind-protect
(cl-letf (((symbol-function 'ai-code-backends-infra--session-buffer-matches-directory-p)
(lambda (_buf _dir) t)))
(should-not (memq session-buf (ai-code--find-project-session-buffers))))
(kill-buffer session-buf)))))

(ert-deftest ai-code-test-prompt-choose-target-session-nil-when-no-visible ()
"Return nil when no visible session buffers."
(cl-letf (((symbol-function 'ai-code--find-visible-session-buffer)
(lambda () nil)))
(should-not (ai-code--prompt-choose-target-session))))

(ert-deftest ai-code-test-prompt-choose-target-session-returns-visible-when-only-option ()
"Return nil when visible session belongs to the same project (default dispatch)."
(let ((session-buf (get-buffer-create "*claude[test]*")))
(unwind-protect
(cl-letf (((symbol-function 'ai-code--find-visible-session-buffer)
(lambda () session-buf))
((symbol-function 'ai-code--find-project-session-buffers)
(lambda () (list session-buf))))
(should-not (ai-code--prompt-choose-target-session)))
(kill-buffer session-buf))))

(ert-deftest ai-code-test-prompt-choose-target-session-returns-visible-when-no-project-session ()
"Return visible session buffer when current project has no sessions."
(let ((visible-buf (get-buffer-create "*gemini[other-project]*")))
(unwind-protect
(cl-letf (((symbol-function 'ai-code--find-visible-session-buffer)
(lambda () visible-buf))
((symbol-function 'ai-code--find-project-session-buffers)
(lambda () nil)))
(should (eq (ai-code--prompt-choose-target-session) visible-buf)))
(kill-buffer visible-buf))))

(ert-deftest ai-code-test-prompt-choose-target-session-asks-when-sessions-differ ()
"Ask user to choose when visible and project sessions differ."
(let ((visible-buf (get-buffer-create "*gemini[other-project]*"))
(project-buf (get-buffer-create "*claude[my-project]*"))
(offered-choices nil))
(unwind-protect
(cl-letf (((symbol-function 'ai-code--find-visible-session-buffer)
(lambda () visible-buf))
((symbol-function 'ai-code--find-project-session-buffers)
(lambda () (list project-buf)))
((symbol-function 'completing-read)
(lambda (_prompt collection &rest _args)
(setq offered-choices collection)
(buffer-name project-buf))))
(let ((result (ai-code--prompt-choose-target-session)))
(should (eq result project-buf))
(should (member (buffer-name visible-buf) offered-choices))
(should (member (buffer-name project-buf) offered-choices))))
(kill-buffer visible-buf)
(kill-buffer project-buf))))

(ert-deftest ai-code-test-send-prompt-uses-visible-session-directly ()
"Send prompt directly to visible session when target is resolved."
(let ((session-buf (get-buffer-create "*claude[test]*"))
(sent-string nil)
(return-sent nil)
(displayed-buffer nil)
(cli-send-called nil))
(unwind-protect
(cl-letf (((symbol-function 'ai-code--prompt-choose-target-session)
(lambda () session-buf))
((symbol-function 'ai-code-backends-infra--terminal-send-string)
(lambda (str) (setq sent-string str)))
((symbol-function 'ai-code-backends-infra--terminal-send-return)
(lambda () (setq return-sent t)))
((symbol-function 'get-buffer-window)
(lambda (_buf &rest _) nil))
((symbol-function 'ai-code-backends-infra--display-buffer-in-side-window)
(lambda (buf) (setq displayed-buffer buf)))
((symbol-function 'ai-code-cli-send-command)
(lambda (_cmd) (setq cli-send-called t)))
((symbol-function 'ai-code-cli-switch-to-buffer)
(lambda () nil))
((symbol-function 'sit-for)
(lambda (_secs) nil)))
(ai-code--send-prompt "test prompt")
(should (string= sent-string "test prompt"))
(should return-sent)
(should (eq displayed-buffer session-buf))
(should-not cli-send-called))
(kill-buffer session-buf))))

(ert-deftest ai-code-test-send-prompt-falls-through-when-no-visible-session ()
"Use default path when no visible session is chosen."
(let ((cli-send-called nil)
(switch-called nil))
(cl-letf (((symbol-function 'ai-code--prompt-choose-target-session)
(lambda () nil))
((symbol-function 'ai-code-cli-send-command)
(lambda (cmd) (setq cli-send-called cmd)))
((symbol-function 'ai-code-cli-switch-to-buffer)
(lambda () (setq switch-called t))))
(ai-code--send-prompt "test prompt")
(should (string= cli-send-called "test prompt"))
(should switch-called))))

(provide 'test-ai-code-prompt-mode)
;;; test_ai-code-prompt-mode.el ends here
Loading