diff options
| -rw-r--r-- | modules/ai-vterm.el | 45 | ||||
| -rw-r--r-- | tests/test-ai-vterm--server-display.el | 127 |
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 |
