From 7833fb8bc0e3f9ece01ab2fe6fe07ded0efc4af4 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Wed, 27 May 2026 20:48:12 -0500 Subject: fix(vterm): never reopen the F9/F12 windows from the top F9 brought the agent window down from the top of the frame. The toggle remembers where the window last sat and replays it, and "above" was a position it could capture and replay: move the window to the top with the buffer-move keys, toggle off, and the next toggle reopened it up there. The host default never picks the top, so a remembered "above" was the only way in. I added an optional allowed-directions list to cj/window-toggle-capture-state, the helper both F9 (ai-vterm) and F12 (vterm-config) share. When the captured direction isn't in the list, it falls back to the default direction and clears the saved size, since that size was measured on the disallowed axis and wouldn't transfer. Both dispatchers now pass (right below left), so neither can remember a top placement. They go through the same helper, so the rule stays in one place. Three tests cover the new branch: a permitted direction is kept, a disallowed one falls back with the size cleared, and an omitted list preserves the old unconstrained behavior so existing callers are unaffected. --- modules/ai-vterm.el | 14 +++++++----- modules/cj-window-toggle-lib.el | 23 ++++++++++++++----- modules/vterm-config.el | 9 +++++--- tests/test-cj-window-toggle-lib.el | 46 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 13 deletions(-) diff --git a/modules/ai-vterm.el b/modules/ai-vterm.el index d932b0e9..70395ecc 100644 --- a/modules/ai-vterm.el +++ b/modules/ai-vterm.el @@ -391,10 +391,13 @@ on a desktop -- pairing with the axis chosen by (defvar cj/--ai-vterm-last-direction nil "Last user-chosen direction for the AI-vterm display. -Symbol: right, below, left, or above. nil means no agent window -has been toggled off yet this session, so the default direction -applies. Captured at toggle-off by `cj/--ai-vterm-capture-state' -and consumed by `cj/--ai-vterm-display-saved'.") +Symbol: right, below, or left. `above' is never stored -- the agent +window must not be remembered at the top of the frame, so a top +placement falls back to the host default at capture time. nil means no +agent window has been toggled off yet this session, so the default +direction applies. Captured at toggle-off by +`cj/--ai-vterm-capture-state' and consumed by +`cj/--ai-vterm-display-saved'.") (defvar cj/--ai-vterm-last-was-bury nil "Non-nil when the last F9 toggle-off used `bury-buffer'. @@ -444,7 +447,8 @@ is not live." (cj/window-toggle-capture-state window (cj/--ai-vterm-default-direction) 'cj/--ai-vterm-last-direction - 'cj/--ai-vterm-last-size)) + 'cj/--ai-vterm-last-size + '(right below left))) (defun cj/--ai-vterm-reuse-existing-agent (buffer _alist) "Display-buffer action: reuse any window in this frame already showing diff --git a/modules/cj-window-toggle-lib.el b/modules/cj-window-toggle-lib.el index ef57e5cf..9874a134 100644 --- a/modules/cj-window-toggle-lib.el +++ b/modules/cj-window-toggle-lib.el @@ -20,7 +20,8 @@ (require 'cj-window-geometry-lib) (defun cj/window-toggle-capture-state (window default-direction - direction-var size-var) + direction-var size-var + &optional allowed) "Write WINDOW's direction and body size into DIRECTION-VAR and SIZE-VAR. DEFAULT-DIRECTION is the symbol used by `cj/window-direction' when @@ -28,12 +29,24 @@ WINDOW fills its frame's root area. DIRECTION-VAR and SIZE-VAR are the symbols of the consumer's state variables; they receive the captured values via `set'. +ALLOWED, when non-nil, is a list of permitted direction symbols. If +WINDOW's captured direction isn't in it, fall back to DEFAULT-DIRECTION +and clear SIZE-VAR (set to nil) so the consumer's default size applies -- +the captured body size was measured on the disallowed axis and wouldn't +transfer meaningfully. A consumer that wants to forbid a placement (e.g. +an agent window that should never be remembered at the top of the frame) +passes the directions it does allow. Omit ALLOWED to keep every +direction. + No-op when WINDOW is nil or not live." (when (window-live-p window) - (let* ((dir (cj/window-direction window default-direction)) - (size (cj/window-body-size window dir))) - (set direction-var dir) - (set size-var size)))) + (let ((dir (cj/window-direction window default-direction))) + (if (or (null allowed) (memq dir allowed)) + (progn + (set direction-var dir) + (set size-var (cj/window-body-size window dir))) + (set direction-var default-direction) + (set size-var nil))))) (defun cj/window-toggle-display-saved (buffer alist direction-var default-direction diff --git a/modules/vterm-config.el b/modules/vterm-config.el index e7965c65..c8a57d30 100644 --- a/modules/vterm-config.el +++ b/modules/vterm-config.el @@ -341,8 +341,10 @@ Used as the size fallback when `cj/--vterm-toggle-last-size' is nil (defvar cj/--vterm-toggle-last-direction nil "Last user-chosen direction for the F12 vterm display. -Symbol: right, left, below, above. nil means use the default -`below' for F12's traditional bottom split.") +Symbol: right, left, or below. `above' is never stored -- a top +placement falls back to `below' at capture time, so F12 never reopens +from the top. nil means use the default `below' for F12's traditional +bottom split.") (defvar cj/--vterm-toggle-last-size nil "Last user-chosen body size for the F12 vterm display. @@ -384,7 +386,8 @@ split when WINDOW fills the frame's root area." (cj/window-toggle-capture-state window 'below 'cj/--vterm-toggle-last-direction - 'cj/--vterm-toggle-last-size)) + 'cj/--vterm-toggle-last-size + '(right below left))) (defun cj/--vterm-toggle-display-saved (buffer alist) "Display-buffer action: split per saved direction and body size. diff --git a/tests/test-cj-window-toggle-lib.el b/tests/test-cj-window-toggle-lib.el index ca4b7fef..0762e255 100644 --- a/tests/test-cj-window-toggle-lib.el +++ b/tests/test-cj-window-toggle-lib.el @@ -91,6 +91,52 @@ (should (eq test-cj-window-toggle--last-direction 'sentinel-dir)) (should (= test-cj-window-toggle--last-size 0.123)))) +(ert-deftest test-cj-window-toggle-capture-allowed-keeps-permitted-direction () + "Normal: a captured direction in ALLOWED is stored with its body size." + (save-window-excursion + (delete-other-windows) + (let ((below (split-window (selected-window) nil 'below)) + (test-cj-window-toggle--last-direction nil) + (test-cj-window-toggle--last-size nil)) + (cj/window-toggle-capture-state + below 'below + 'test-cj-window-toggle--last-direction + 'test-cj-window-toggle--last-size + '(right below left)) + (should (eq test-cj-window-toggle--last-direction 'below)) + (should (integerp test-cj-window-toggle--last-size))))) + +(ert-deftest test-cj-window-toggle-capture-allowed-rejects-disallowed-direction () + "Boundary: a direction not in ALLOWED falls back to default, size cleared. +The captured body size was measured on the disallowed axis, so it can't +transfer; clearing it lets the consumer's default size apply." + (save-window-excursion + (delete-other-windows) + (let ((above (split-window (selected-window) nil 'above)) + (test-cj-window-toggle--last-direction 'sentinel) + (test-cj-window-toggle--last-size 99)) + (cj/window-toggle-capture-state + above 'below + 'test-cj-window-toggle--last-direction + 'test-cj-window-toggle--last-size + '(right below left)) + (should (eq test-cj-window-toggle--last-direction 'below)) + (should (null test-cj-window-toggle--last-size))))) + +(ert-deftest test-cj-window-toggle-capture-allowed-nil-keeps-all () + "Boundary: omitting ALLOWED preserves the prior unconstrained behavior." + (save-window-excursion + (delete-other-windows) + (let ((above (split-window (selected-window) nil 'above)) + (test-cj-window-toggle--last-direction nil) + (test-cj-window-toggle--last-size nil)) + (cj/window-toggle-capture-state + above 'below + 'test-cj-window-toggle--last-direction + 'test-cj-window-toggle--last-size) + (should (eq test-cj-window-toggle--last-direction 'above)) + (should (integerp test-cj-window-toggle--last-size))))) + (ert-deftest test-cj-window-toggle-display-saved-uses-defaults-when-state-nil () "Normal: nil state -> direction=edge of default, size=default." (let (received-alist -- cgit v1.2.3