aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-vterm.el45
-rw-r--r--tests/test-ai-vterm--server-display.el127
2 files changed, 172 insertions, 0 deletions
diff --git a/modules/ai-vterm.el b/modules/ai-vterm.el
index 9e2f9774..22c9b5e9 100644
--- a/modules/ai-vterm.el
+++ b/modules/ai-vterm.el
@@ -650,5 +650,50 @@ AI-vterm buffers without touching the project list."
(keymap-global-set "C-<f9>" #'cj/ai-vterm-pick-project)
(keymap-global-set "M-<f9>" #'cj/ai-vterm-pick-buffer)
+;; ---------- emacsclient: keep opened files off the agent vterm ----------
+;;
+;; `server-start' (in system-defaults.el) leaves `server-window' nil, so
+;; `server-switch-buffer' opens an `emacsclient -n' file in the *selected*
+;; window. When the user is typing in the agent vterm, that's the agent
+;; window -- so "tell the agent to open X" would replace the agent buffer
+;; with X. The function below, wired as `server-window', routes such files
+;; into a non-agent window instead (splitting one off the agent when the
+;; agent is the only window). emacsclient invocations from anywhere else
+;; fall through to `pop-to-buffer' and behave as before.
+
+(defun cj/--ai-vterm-non-agent-window (&optional exclude)
+ "Return a window in the selected frame fit to show a non-agent buffer.
+
+Skips the minibuffer, the EXCLUDE window, dedicated windows, and any
+window already showing an AI-vterm agent buffer. Returns nil when no
+such window exists."
+ (seq-find (lambda (w)
+ (and (not (eq w exclude))
+ (not (window-dedicated-p w))
+ (not (cj/--ai-vterm-buffer-p (window-buffer w)))))
+ (window-list (selected-frame) 'never)))
+
+(defun cj/--ai-vterm-server-display (buffer)
+ "Display BUFFER for `server-window', keeping it off the agent vterm.
+
+When the selected window shows an AI-vterm agent buffer, put BUFFER in
+a non-agent window (`cj/--ai-vterm-non-agent-window'), splitting a
+left-side window off the agent when the agent is the only window, then
+select that window. Otherwise hand off to `pop-to-buffer'. Returns
+the window BUFFER ends up in -- the value `server-switch-buffer'
+expects from a `server-window' function."
+ (if (cj/--ai-vterm-buffer-p (window-buffer (selected-window)))
+ (let* ((agent-win (selected-window))
+ (target (or (cj/--ai-vterm-non-agent-window agent-win)
+ (split-window agent-win nil 'left))))
+ (set-window-buffer target buffer)
+ (select-window target))
+ (pop-to-buffer buffer)
+ (selected-window)))
+
+(defvar server-window)
+(with-eval-after-load 'server
+ (setq server-window #'cj/--ai-vterm-server-display))
+
(provide 'ai-vterm)
;;; ai-vterm.el ends here
diff --git a/tests/test-ai-vterm--server-display.el b/tests/test-ai-vterm--server-display.el
new file mode 100644
index 00000000..1d0d1001
--- /dev/null
+++ b/tests/test-ai-vterm--server-display.el
@@ -0,0 +1,127 @@
+;;; test-ai-vterm--server-display.el --- Tests for emacsclient window routing -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--ai-vterm-server-display' is wired as `server-window' so a file
+;; opened via `emacsclient -n' (e.g. when Craig tells the agent to open
+;; something) doesn't land on top of the agent vterm. When the selected
+;; window shows an `agent [...]' buffer, the file goes to a non-agent
+;; window instead -- splitting one off the agent if it is the only window.
+;; `cj/--ai-vterm-non-agent-window' picks that window.
+
+;;; Code:
+
+(require 'ert)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'ai-vterm)
+(require 'server)
+(require 'testutil-vterm-buffers)
+
+(ert-deftest test-ai-vterm--non-agent-window-finds-code-window ()
+ "Normal: agent on the right, code on the left -> returns the code window."
+ (cj/test--kill-agent-buffers)
+ (let ((agent (get-buffer-create "agent [proj]"))
+ (code (get-buffer-create "code.el")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) code)
+ (let ((right (split-window-right)))
+ (set-window-buffer right agent)
+ (let ((found (cj/--ai-vterm-non-agent-window right)))
+ (should (windowp found))
+ (should (eq (window-buffer found) code)))))
+ (kill-buffer agent)
+ (kill-buffer code))))
+
+(ert-deftest test-ai-vterm--non-agent-window-none-when-only-agent ()
+ "Boundary: the agent window is the only one -> nil."
+ (cj/test--kill-agent-buffers)
+ (let ((agent (get-buffer-create "agent [solo]")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) agent)
+ (should-not (cj/--ai-vterm-non-agent-window (selected-window))))
+ (kill-buffer agent))))
+
+(ert-deftest test-ai-vterm--non-agent-window-skips-dedicated ()
+ "Boundary: a dedicated non-agent window is not a valid target."
+ (cj/test--kill-agent-buffers)
+ (let ((agent (get-buffer-create "agent [proj]"))
+ (side (get-buffer-create "*dedicated-side*")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) agent)
+ (let ((w (split-window-right)))
+ (set-window-buffer w side)
+ (set-window-dedicated-p w t)
+ (unwind-protect
+ (should-not (cj/--ai-vterm-non-agent-window (selected-window)))
+ (set-window-dedicated-p w nil))))
+ (kill-buffer agent)
+ (kill-buffer side))))
+
+(ert-deftest test-ai-vterm--server-display-routes-around-agent ()
+ "Normal: selected window is the agent -> the file lands in the other
+window and the agent window keeps the agent buffer."
+ (cj/test--kill-agent-buffers)
+ (let ((agent (get-buffer-create "agent [proj]"))
+ (code (get-buffer-create "code.el"))
+ (file (get-buffer-create "opened-by-emacsclient.el")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) code)
+ (let ((agent-win (split-window-right)))
+ (set-window-buffer agent-win agent)
+ (select-window agent-win)
+ (cj/--ai-vterm-server-display file)
+ (should (eq (window-buffer agent-win) agent))
+ (should (get-buffer-window file))
+ (should-not (eq (get-buffer-window file) agent-win))))
+ (kill-buffer agent)
+ (kill-buffer code)
+ (kill-buffer file))))
+
+(ert-deftest test-ai-vterm--server-display-splits-when-agent-is-only-window ()
+ "Boundary: the agent is the only window -> a window is split off for the
+file; the agent window keeps the agent buffer."
+ (cj/test--kill-agent-buffers)
+ (let ((agent (get-buffer-create "agent [solo]"))
+ (file (get-buffer-create "opened.el")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) agent)
+ (let ((agent-win (selected-window)))
+ (cj/--ai-vterm-server-display file)
+ (should (= 2 (length (window-list (selected-frame) 'never))))
+ (should (eq (window-buffer agent-win) agent))
+ (should (eq (window-buffer (get-buffer-window file)) file))))
+ (kill-buffer agent)
+ (kill-buffer file))))
+
+(ert-deftest test-ai-vterm--server-display-passthrough-when-not-agent ()
+ "Normal: selected window is a regular buffer -> the file is displayed
+normally and nothing special happens (no agent window to protect)."
+ (cj/test--kill-agent-buffers)
+ (let ((code (get-buffer-create "code.el"))
+ (file (get-buffer-create "opened.el")))
+ (unwind-protect
+ (save-window-excursion
+ (delete-other-windows)
+ (set-window-buffer (selected-window) code)
+ (cj/--ai-vterm-server-display file)
+ (should (get-buffer-window file)))
+ (kill-buffer code)
+ (kill-buffer file))))
+
+(ert-deftest test-ai-vterm--server-window-wired-to-helper ()
+ "Normal: the module sets `server-window' to its display function."
+ (should (eq server-window #'cj/--ai-vterm-server-display)))
+
+(provide 'test-ai-vterm--server-display)
+;;; test-ai-vterm--server-display.el ends here