diff options
| -rw-r--r-- | modules/ai-vterm.el | 70 | ||||
| -rw-r--r-- | tests/test-ai-vterm--default-geometry.el | 56 | ||||
| -rw-r--r-- | tests/test-ai-vterm--display-rule.el | 22 | ||||
| -rw-r--r-- | tests/test-ai-vterm--display-saved.el | 29 |
4 files changed, 145 insertions, 32 deletions
diff --git a/modules/ai-vterm.el b/modules/ai-vterm.el index 4a8f7e0e..266966ea 100644 --- a/modules/ai-vterm.el +++ b/modules/ai-vterm.el @@ -7,10 +7,13 @@ ;; Picks an AI-agent project (a dir under ~/.emacs.d, ~/code/*, or ;; ~/projects/* containing .ai/protocols.org), opens or reuses a vterm ;; buffer named "agent [<basename>]", sends the agent's startup -;; instruction to it, and routes the buffer to a right-side window via -;; display-buffer-alist. Multiple projects produce multiple coexisting -;; buffers that share the same right-side slot; switching among them is a -;; buffer-switch, not a kill-and-recreate. +;; instruction to it, and routes the buffer to a side window via +;; display-buffer-alist. The default placement is host-aware: a +;; right-side split at 50% width on a desktop, a bottom split at 75% +;; height on a laptop (see `cj/--ai-vterm-default-direction'). Multiple +;; projects produce multiple coexisting buffers that share the same +;; slot; switching among them is a buffer-switch, not a +;; kill-and-recreate. ;; ;; Each project's agent runs inside a tmux session named ;; "<cj/ai-vterm-tmux-session-prefix><basename>" (default prefix "aiv-"). @@ -47,6 +50,7 @@ (require 'seq) (require 'cj-window-geometry-lib) (require 'cj-window-toggle-lib) +(require 'host-environment) (declare-function vterm "vterm" (&optional buffer-name)) (declare-function vterm-send-string "vterm" (string &optional paste-p)) @@ -332,17 +336,44 @@ the active group alphabetical too." (let ((proc (get-buffer-process buffer))) (and proc (process-live-p proc)))) -(defcustom cj/ai-vterm-window-width 0.5 - "Default fraction of frame allocated to the AI-vterm window. +(defcustom cj/ai-vterm-desktop-width 0.5 + "Default fraction of frame width for the AI-vterm window on a desktop. -Used by `cj/--ai-vterm-display-saved' as the size fallback when -`cj/--ai-vterm-last-size' is nil (i.e. the user hasn't yet toggled -off an agent window in this session). Applies to both width and -height axes -- the same fallback fraction is used for either default -direction." +On a desktop the agent opens as a right-side vertical split (see +`cj/--ai-vterm-default-direction'), so this fraction is interpreted +as a window width. Used by `cj/--ai-vterm-default-size' as the size +fallback when `cj/--ai-vterm-last-size' is nil (i.e. the user hasn't +yet toggled off an agent window in this session)." :type 'number :group 'ai-vterm) +(defcustom cj/ai-vterm-laptop-height 0.75 + "Default fraction of frame height for the AI-vterm window on a laptop. + +On a laptop the agent opens as a bottom horizontal split (see +`cj/--ai-vterm-default-direction'), so this fraction is interpreted +as a window height. Used by `cj/--ai-vterm-default-size' as the size +fallback when `cj/--ai-vterm-last-size' is nil." + :type 'number + :group 'ai-vterm) + +(defun cj/--ai-vterm-default-direction () + "Return the host-appropriate default split direction for the agent window. + +`below' on a laptop (bottom horizontal split), `right' on a desktop +(right-side vertical split). Detected via `env-laptop-p'." + (if (env-laptop-p) 'below 'right)) + +(defun cj/--ai-vterm-default-size () + "Return the host-appropriate default size fraction for the agent window. + +`cj/ai-vterm-laptop-height' on a laptop, `cj/ai-vterm-desktop-width' +on a desktop -- pairing with the axis chosen by +`cj/--ai-vterm-default-direction'." + (if (env-laptop-p) + cj/ai-vterm-laptop-height + cj/ai-vterm-desktop-width)) + (defvar cj/--ai-vterm-last-direction nil "Last user-chosen direction for the AI-vterm display. @@ -366,7 +397,7 @@ the saved direction.") Positive integer: body-columns when `cj/--ai-vterm-last-direction' is right or left, body-lines when below or above. nil means use -the customizable default `cj/ai-vterm-window-width' (a float +the host-aware default from `cj/--ai-vterm-default-size' (a float fraction). Body size, not total size, because total-width includes the @@ -392,10 +423,12 @@ cost of not auto-scaling if the frame itself resizes.") Sets `cj/--ai-vterm-last-direction' and `cj/--ai-vterm-last-size' so a subsequent F9 display can restore the user's chosen orientation and size. Called at toggle-off (just before the window is torn -down). The default direction is `right' -- the module's side-panel -default. Does nothing when WINDOW is not live." +down). The default direction is host-aware via +`cj/--ai-vterm-default-direction' (used only when WINDOW fills its +frame and no direction can be inferred). Does nothing when WINDOW +is not live." (cj/window-toggle-capture-state - window 'right + window (cj/--ai-vterm-default-direction) 'cj/--ai-vterm-last-direction 'cj/--ai-vterm-last-size)) @@ -429,7 +462,8 @@ than splitting -- preserves the user's lone-window layout across F9 toggles. Otherwise delegates to `cj/window-toggle-display-saved' against the -F9 state vars, falling back to `right' and `cj/ai-vterm-window-width'." +F9 state vars, falling back to the host-aware defaults from +`cj/--ai-vterm-default-direction' and `cj/--ai-vterm-default-size'." (cond ((and cj/--ai-vterm-last-was-bury (one-window-p)) (setq cj/--ai-vterm-last-was-bury nil) @@ -440,8 +474,8 @@ F9 state vars, falling back to `right' and `cj/ai-vterm-window-width'." (setq cj/--ai-vterm-last-was-bury nil) (cj/window-toggle-display-saved buffer alist - 'cj/--ai-vterm-last-direction 'right - 'cj/--ai-vterm-last-size cj/ai-vterm-window-width)))) + 'cj/--ai-vterm-last-direction (cj/--ai-vterm-default-direction) + 'cj/--ai-vterm-last-size (cj/--ai-vterm-default-size))))) (defun cj/--ai-vterm-display-rule-list () "Return the `display-buffer-alist' entry list installed by this module. diff --git a/tests/test-ai-vterm--default-geometry.el b/tests/test-ai-vterm--default-geometry.el new file mode 100644 index 00000000..f8ec08c9 --- /dev/null +++ b/tests/test-ai-vterm--default-geometry.el @@ -0,0 +1,56 @@ +;;; test-ai-vterm--default-geometry.el --- Tests for host-aware display defaults -*- lexical-binding: t; -*- + +;;; Commentary: +;; ai-vterm's default display geometry is host-aware: a laptop opens the +;; agent from the bottom (75% height), a desktop opens it from the right +;; (50% width). `cj/--ai-vterm-default-direction' and +;; `cj/--ai-vterm-default-size' encapsulate the `env-laptop-p' branch; +;; they feed the default fallbacks in `cj/--ai-vterm-capture-state' and +;; `cj/--ai-vterm-display-saved'. +;; +;; `env-laptop-p' is stubbed per-test so the assertions are deterministic +;; regardless of the host the suite runs on. + +;;; Code: + +(require 'ert) +(require 'cl-lib) + +(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) +(require 'ai-vterm) + +(ert-deftest test-ai-vterm--default-direction-laptop () + "Normal: on a laptop the default direction is `below'." + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (eq (cj/--ai-vterm-default-direction) 'below)))) + +(ert-deftest test-ai-vterm--default-direction-desktop () + "Normal: on a desktop the default direction is `right'." + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (eq (cj/--ai-vterm-default-direction) 'right)))) + +(ert-deftest test-ai-vterm--default-size-laptop () + "Normal: on a laptop the default size is `cj/ai-vterm-laptop-height'." + (let ((cj/ai-vterm-laptop-height 0.75) + (cj/ai-vterm-desktop-width 0.5)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (= (cj/--ai-vterm-default-size) 0.75))))) + +(ert-deftest test-ai-vterm--default-size-desktop () + "Normal: on a desktop the default size is `cj/ai-vterm-desktop-width'." + (let ((cj/ai-vterm-laptop-height 0.75) + (cj/ai-vterm-desktop-width 0.5)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (= (cj/--ai-vterm-default-size) 0.5))))) + +(ert-deftest test-ai-vterm--default-size-respects-custom-values () + "Boundary: the helper returns the customized values, not the literals." + (let ((cj/ai-vterm-laptop-height 0.6) + (cj/ai-vterm-desktop-width 0.33)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t))) + (should (= (cj/--ai-vterm-default-size) 0.6))) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (should (= (cj/--ai-vterm-default-size) 0.33))))) + +(provide 'test-ai-vterm--default-geometry) +;;; test-ai-vterm--default-geometry.el ends here diff --git a/tests/test-ai-vterm--display-rule.el b/tests/test-ai-vterm--display-rule.el index 15d270e2..9b70134a 100644 --- a/tests/test-ai-vterm--display-rule.el +++ b/tests/test-ai-vterm--display-rule.el @@ -9,6 +9,7 @@ ;;; Code: (require 'ert) +(require 'cl-lib) (add-to-list 'load-path (expand-file-name "modules" user-emacs-directory)) (require 'ai-vterm) @@ -27,19 +28,22 @@ ,@body))) (ert-deftest test-ai-vterm--display-rule-routes-agent-buffer-to-right () - "Normal: a buffer named \"agent [foo]\" lands in a window to the right. + "Normal: on a desktop, \"agent [foo]\" lands in a window to the right. -The rule uses `display-buffer-in-direction' with `(direction . right)', -which splits the current window so the new window's left edge sits at -a positive column. The buffer winds up in that new window." +The desktop default direction is `right' (see +`cj/--ai-vterm-default-direction'), so the rule splits the current +window with `(direction . right)' and the new window's left edge +sits at a positive column. `env-laptop-p' is stubbed nil to pin the +desktop branch; on a laptop the agent would land below instead." (let ((name "agent [display-rule-test]")) (test-ai-vterm--cleanup name) (unwind-protect - (test-ai-vterm--with-clean-frame - (let* ((buf (get-buffer-create name)) - (win (display-buffer buf))) - (should (windowp win)) - (should (> (window-left-column win) 0)))) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil))) + (test-ai-vterm--with-clean-frame + (let* ((buf (get-buffer-create name)) + (win (display-buffer buf))) + (should (windowp win)) + (should (> (window-left-column win) 0))))) (test-ai-vterm--cleanup name)))) (ert-deftest test-ai-vterm--display-rule-skips-non-matching-buffer () diff --git a/tests/test-ai-vterm--display-saved.el b/tests/test-ai-vterm--display-saved.el index a17df0aa..91cea46e 100644 --- a/tests/test-ai-vterm--display-saved.el +++ b/tests/test-ai-vterm--display-saved.el @@ -20,16 +20,18 @@ (require 'ai-vterm) (require 'testutil-vterm-buffers) -(ert-deftest test-ai-vterm--display-saved-uses-defaults-when-state-nil () - "Normal: nil state -> direction=rightmost, size=cj/ai-vterm-window-width. +(ert-deftest test-ai-vterm--display-saved-uses-desktop-defaults-when-state-nil () + "Normal: nil state on a desktop -> rightmost, size=cj/ai-vterm-desktop-width. The cardinal `right' default maps to the frame-edge variant `rightmost' so agent lands at the frame's right edge regardless of -which window is selected." +which window is selected. `env-laptop-p' is stubbed nil to pin the +desktop branch." (let (received-buf received-alist (cj/--ai-vterm-last-direction nil) (cj/--ai-vterm-last-size nil) - (cj/ai-vterm-window-width 0.5)) - (cl-letf (((symbol-function 'display-buffer-in-direction) + (cj/ai-vterm-desktop-width 0.5)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () nil)) + ((symbol-function 'display-buffer-in-direction) (lambda (b a) (setq received-buf b received-alist a) 'fake-window))) @@ -39,6 +41,23 @@ which window is selected." (should (= (cdr (assq 'window-width received-alist)) 0.5)) (should (eq (cdr (assq 'inhibit-same-window received-alist)) t)))) +(ert-deftest test-ai-vterm--display-saved-uses-laptop-defaults-when-state-nil () + "Normal: nil state on a laptop -> bottom, size=cj/ai-vterm-laptop-height. +The cardinal `below' default maps to the frame-edge variant `bottom' +and the size lands on the `window-height' axis. `env-laptop-p' is +stubbed t to pin the laptop branch." + (let (received-alist + (cj/--ai-vterm-last-direction nil) + (cj/--ai-vterm-last-size nil) + (cj/ai-vterm-laptop-height 0.75)) + (cl-letf (((symbol-function 'env-laptop-p) (lambda () t)) + ((symbol-function 'display-buffer-in-direction) + (lambda (_b a) (setq received-alist a) 'fake-window))) + (cj/--ai-vterm-display-saved 'fake-buf '((inhibit-same-window . t)))) + (should (eq (cdr (assq 'direction received-alist)) 'bottom)) + (should (= (cdr (assq 'window-height received-alist)) 0.75)) + (should-not (assq 'window-width received-alist)))) + (ert-deftest test-ai-vterm--display-saved-uses-saved-direction-and-size-below () "Normal: saved direction=below maps to bottom edge; size=0.4 passes through." (let (received-alist |
