aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-01 21:59:34 -0400
committerCraig Jennings <c@cjennings.net>2026-07-01 21:59:34 -0400
commit110440b6b3632b9bf0a9e5cf0838c9891f61e17a (patch)
treeb717505d29109ce62afdb7e614b4a66791a4348b
parente281d88b16fa9c0b8faf5243dfc21481ac3cba9d (diff)
downloaddotemacs-110440b6b3632b9bf0a9e5cf0838c9891f61e17a.tar.gz
dotemacs-110440b6b3632b9bf0a9e5cf0838c9891f61e17a.zip
feat(ai-term): say so when M-SPC has no other agent to switch to
With a single agent open and focused, the rotation wrapped back to the same agent and echoed a misleading "Agent: <name>" as if it had swapped. Now it says there are no other ai-terms to switch to. A sole agent that is displayed but not selected still gets selected, and the no-agents picker fallback is unchanged.
-rw-r--r--modules/ai-term.el23
-rw-r--r--tests/test-ai-term--next-single-agent.el125
2 files changed, 142 insertions, 6 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el
index 6dfb669a..ecc2842b 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -1007,7 +1007,8 @@ its terminal, which reattaches the session). When an agent window is on
screen, swap it to the next agent (wrapping after the last) and select it.
When no agent is displayed but agents exist, show the first. When none
are open, open the project picker to launch the first agent rather than
-erroring.
+erroring. When the sole agent is already focused, echo that there are
+no other ai-terms to switch to instead of swapping to itself.
Bound to M-SPC. Unlike C-; a a (toggle the most-recent agent on/off), this
is the \"switch among existing agents\" surface; C-; a s opens the project
@@ -1021,10 +1022,20 @@ picker and C-; a k closes an agent."
(equal (cj/--ai-term-buffer-name d) current-name))
dirs)))
(next-dir (cj/--ai-term-next-agent-dir current-dir dirs)))
- (if (not next-dir)
- ;; No agents open: launch the first via the project picker instead of
- ;; erroring, so the swap key doubles as a "start an agent" key.
- (cj/ai-term-pick-project)
+ (cond
+ ((not next-dir)
+ ;; No agents open: launch the first via the project picker instead of
+ ;; erroring, so the swap key doubles as a "start an agent" key.
+ (cj/ai-term-pick-project))
+ ;; Sole agent, already focused: the rotation wraps back to the same
+ ;; agent, so a swap would be a no-op with a misleading "Agent:" echo.
+ ;; Say there's nowhere to go instead. A sole agent that is displayed
+ ;; but not selected still falls through and gets selected.
+ ((and current-dir
+ (equal next-dir current-dir)
+ (eq win (selected-window)))
+ (message "No other ai-terms to switch to"))
+ (t
(let* ((name (cj/--ai-term-buffer-name next-dir))
(existing (get-buffer name)))
;; Live agent and an agent window is up: swap it into that window in
@@ -1035,7 +1046,7 @@ picker and C-; a k closes an agent."
(cj/--ai-term-show-or-create next-dir name)
(let ((w (get-buffer-window name)))
(when w (select-window w))))
- (message "Agent: %s" name)))))
+ (message "Agent: %s" name))))))
;; ai-term lives under the C-; a prefix (vacated when gptel was archived).
;; The frequent "swap to the next agent" also gets M-SPC for a fast chord.
diff --git a/tests/test-ai-term--next-single-agent.el b/tests/test-ai-term--next-single-agent.el
new file mode 100644
index 00000000..f31807d0
--- /dev/null
+++ b/tests/test-ai-term--next-single-agent.el
@@ -0,0 +1,125 @@
+;;; test-ai-term--next-single-agent.el --- cj/ai-term-next sole-agent message -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; When M-SPC fires with a single ai-term open and that agent's window
+;; selected, `cj/ai-term-next' has nowhere to go: the rotation wraps back
+;; to the same agent. Instead of a misleading "Agent: <name>" swap
+;; message, it should say there are no other ai-terms to switch to.
+;; A sole agent that is displayed but NOT selected still gets selected
+;; (the swap key doubles as "take me to the agent"), and a sole agent
+;; with no window still gets shown.
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'ai-term)
+
+(defun test-ai-term-next-single--buffer-name (dir)
+ "Deterministic fake agent buffer name for DIR."
+ (concat "agent-buf-" dir))
+
+(ert-deftest test-ai-term-next-single-agent-focused-messages-no-others ()
+ "Normal: sole agent focused -> echo 'no other ai-terms', no swap."
+ (let ((buf (get-buffer-create "agent-buf-a"))
+ (captured nil)
+ (shown 0))
+ (unwind-protect
+ (progn
+ (set-window-buffer (selected-window) buf)
+ (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs)
+ (lambda (&rest _) '("a")))
+ ((symbol-function 'cj/--ai-term-displayed-agent-window)
+ (lambda (&rest _) (selected-window)))
+ ((symbol-function 'cj/--ai-term-buffer-name)
+ #'test-ai-term-next-single--buffer-name)
+ ((symbol-function 'cj/--ai-term-process-live-p)
+ (lambda (&rest _) t))
+ ((symbol-function 'cj/--ai-term-show-or-create)
+ (lambda (&rest _) (setq shown (1+ shown))))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (when fmt (setq captured (apply #'format fmt args))))))
+ (cj/ai-term-next)
+ (should (equal captured "No other ai-terms to switch to"))
+ (should (= shown 0))
+ (should (eq (window-buffer (selected-window)) buf))))
+ (kill-buffer buf))))
+
+(ert-deftest test-ai-term-next-single-agent-undisplayed-shows-it ()
+ "Normal: sole agent with no window -> shown, not the no-others message."
+ (let ((captured nil)
+ (shown 0))
+ (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs)
+ (lambda (&rest _) '("a")))
+ ((symbol-function 'cj/--ai-term-displayed-agent-window)
+ (lambda (&rest _) nil))
+ ((symbol-function 'cj/--ai-term-buffer-name)
+ #'test-ai-term-next-single--buffer-name)
+ ((symbol-function 'cj/--ai-term-process-live-p)
+ (lambda (&rest _) t))
+ ((symbol-function 'cj/--ai-term-show-or-create)
+ (lambda (&rest _) (setq shown (1+ shown))))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (when fmt (setq captured (apply #'format fmt args))))))
+ (cj/ai-term-next)
+ (should (= shown 1))
+ (should-not (equal captured "No other ai-terms to switch to")))))
+
+(ert-deftest test-ai-term-next-single-agent-unselected-window-gets-selected ()
+ "Boundary: sole agent displayed in another window -> selected, no message change."
+ (skip-unless (not noninteractive)) ; window splitting is unreliable in batch
+ (let ((buf (get-buffer-create "agent-buf-a"))
+ (captured nil))
+ (unwind-protect
+ (let* ((w1 (selected-window))
+ (w2 (split-window)))
+ (set-window-buffer w2 buf)
+ (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs)
+ (lambda (&rest _) '("a")))
+ ((symbol-function 'cj/--ai-term-displayed-agent-window)
+ (lambda (&rest _) w2))
+ ((symbol-function 'cj/--ai-term-buffer-name)
+ #'test-ai-term-next-single--buffer-name)
+ ((symbol-function 'cj/--ai-term-process-live-p)
+ (lambda (&rest _) t))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (when fmt (setq captured (apply #'format fmt args))))))
+ (select-window w1)
+ (cj/ai-term-next)
+ (should (eq (selected-window) w2))
+ (should-not (equal captured "No other ai-terms to switch to"))))
+ (delete-other-windows)
+ (kill-buffer buf))))
+
+(ert-deftest test-ai-term-next-two-agents-still-swaps ()
+ "Normal: two agents, one focused -> swaps to the other, no no-others message."
+ (let ((buf-a (get-buffer-create "agent-buf-a"))
+ (buf-b (get-buffer-create "agent-buf-b"))
+ (captured nil))
+ (unwind-protect
+ (progn
+ (set-window-buffer (selected-window) buf-a)
+ (cl-letf (((symbol-function 'cj/--ai-term-active-agent-dirs)
+ (lambda (&rest _) '("a" "b")))
+ ((symbol-function 'cj/--ai-term-displayed-agent-window)
+ (lambda (&rest _) (selected-window)))
+ ((symbol-function 'cj/--ai-term-buffer-name)
+ #'test-ai-term-next-single--buffer-name)
+ ((symbol-function 'cj/--ai-term-process-live-p)
+ (lambda (&rest _) t))
+ ((symbol-function 'message)
+ (lambda (fmt &rest args)
+ (when fmt (setq captured (apply #'format fmt args))))))
+ (cj/ai-term-next)
+ (should (eq (window-buffer (selected-window)) buf-b))
+ (should (equal captured "Agent: agent-buf-b"))))
+ (kill-buffer buf-a)
+ (kill-buffer buf-b))))
+
+(provide 'test-ai-term--next-single-agent)
+;;; test-ai-term--next-single-agent.el ends here