Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
69 changes: 66 additions & 3 deletions ai-code-prompt-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,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,13 +181,71 @@ 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 an AI session buffer visible in the current frame, or nil."
(cl-some
(lambda (win)
(let ((buf (window-buffer win)))
(when (and (buffer-live-p buf)
(ai-code-backends-infra--session-buffer-p buf))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard session routing when infra helpers are unavailable

ai-code--send-prompt now always enters ai-code--prompt-choose-target-session, and this path unconditionally calls ai-code-backends-infra helpers. In entrypoints that load ai-code-prompt-mode without loading ai-code-backends-infra first (for example, module-level usage paths that don't go through ai-code.el), this can raise void-function ai-code-backends-infra--session-buffer-p before the normal ai-code-cli-send-command fallback runs. Please gate this routing behind fboundp/require so prompt sending still works when infra hasn't been loaded yet.

Useful? React with 👍 / 👎.

buf)))
(window-list nil 'no-minibuffer)))

(defun ai-code--find-project-session-buffers ()
"Return 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)
(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 when a non-default target is chosen, nil for default."
(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."
;; DONE: right now it can only sent prompt to buffer associated ai coding session, which belong to the same git repo. If there is already a AI coding session buffer opened in side panel in same window, it should allow the prompt send to it. If there is already a ai coding session opened for the triggered buffer, but current side panel shows a different session, it should ask user to choose which session it want to send to.
;; DONE: if the current side panel ai coding session is in the same git repo of the triggered buffer file, it should go through the same code path as before, no need to ask user to select session
(let* ((suffix-parts (delq nil (list ai-code-prompt-suffix
(when ai-code-auto-test-type
ai-code-auto-test-suffix)
Expand Down
137 changes: 137 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,142 @@ 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
(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-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
(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
(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-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