summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-11 09:30:53 -0500
committerCraig Jennings <c@cjennings.net>2026-05-11 09:30:53 -0500
commit071fb5e972a08e4072d9177d493928ceb26763f4 (patch)
tree19e51f749b9a92fe77ea1f9288d1200a59e8b67f
parenta70bb985c86aee2b701b40d5c3fae720863cfa4e (diff)
downloaddotemacs-071fb5e972a08e4072d9177d493928ceb26763f4.tar.gz
dotemacs-071fb5e972a08e4072d9177d493928ceb26763f4.zip
feat(ai-vterm): keep emacsclient files out of the agent window
`server-start' leaves `server-window' nil, so `server-switch-buffer' opens an `emacsclient -n' file in the selected window. When I'm typing in the agent vterm, the selected window is the agent window, so "tell the agent to open something" replaced the agent buffer with that file. I wired `server-window' to a function. When the selected window shows an `agent [...]' buffer, it puts the file in a non-agent window instead, splitting one off to the left of the agent when the agent is the only window. emacsclient invocations from anywhere else still go through `pop-to-buffer' unchanged. `cj/--ai-vterm-non-agent-window' picks the target window. It skips the minibuffer, dedicated windows, and any window already showing an agent buffer.
-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