diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-11 09:30:53 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-11 09:30:53 -0500 |
| commit | 071fb5e972a08e4072d9177d493928ceb26763f4 (patch) | |
| tree | 19e51f749b9a92fe77ea1f9288d1200a59e8b67f | |
| parent | a70bb985c86aee2b701b40d5c3fae720863cfa4e (diff) | |
| download | dotemacs-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.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 |
