diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-20 16:00:38 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-20 16:00:38 -0400 |
| commit | 64916462f5c81b854d3fbc6f5c543ecf5ac25bf5 (patch) | |
| tree | 4ac1275810afc50d6b52f90b534cd99d612c316b | |
| parent | 2b7a3139e8f2aef5ca2e072a29e1892f517f77b7 (diff) | |
| download | dotemacs-64916462f5c81b854d3fbc6f5c543ecf5ac25bf5.tar.gz dotemacs-64916462f5c81b854d3fbc6f5c543ecf5ac25bf5.zip | |
fix(ai-term): keep the F9 toggle reversible in a 3+ window layout
In a layout where the agent had its own split window (e.g. code on top, a
working window, and the agent below), toggling the agent off deleted its
window correctly, but toggling back on reused the working window at the edge
-- displacing its buffer and collapsing three windows to two. The slot-reuse
that avoids a third window on a fresh show was firing on a re-show after the
agent's own window was already deleted.
Flag the toggle-off that deletes the agent's own window; on the next
toggle-on, reuse-edge-window consumes the flag and falls through to a fresh
re-split, so the agent returns to its own window and the other windows are
untouched. The flag only changes the 3+ window case -- after a delete in a
2-window slot-reuse layout one window remains, where re-split and reuse-edge
already coincide, so the existing reuse-edge tests are unaffected.
| -rw-r--r-- | modules/ai-term.el | 45 | ||||
| -rw-r--r-- | tests/test-ai-term--reuse-edge-window.el | 41 |
2 files changed, 77 insertions, 9 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el index 55728a3c4..25e56c508 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -433,6 +433,18 @@ without deleting), nil when the window was deleted. Consumed by buried agent in the current window (the only one) or splitting per the saved direction.") +(defvar cj/--ai-term-last-toggle-deleted-split nil + "Non-nil when the last F9 toggle-off deleted the agent's own split window. + +Set t by `cj/--ai-term-toggle-off' only when it actually `delete-window's +the agent (a multi-window layout where the agent had its own window); +nil for a bury or a degenerate swap. Consumed by +`cj/--ai-term-reuse-edge-window': when set, the next toggle-on re-splits a +fresh agent window instead of reusing a window at the edge. Without this, +toggling the agent off and on in a 3+ window layout would reuse the user's +working window at the edge, displacing its buffer and collapsing the layout +-- the toggle must be reversible (off then on returns the same windows).") + (defvar cj/--ai-term-last-hidden-buffer nil "The agent buffer hidden by the most recent F9 toggle-off. @@ -534,14 +546,22 @@ displaced buffer and the agent, never changing the window count. Runs after `cj/--ai-term-reuse-existing-agent', so an agent already on screen has been handled already; the window reused here always holds a -non-agent buffer, which is replaced (it stays alive, just unshown)." - (let* ((direction (or cj/--ai-term-last-direction - (cj/--ai-term-default-direction))) - (win (cj/window-at-edge direction))) - (when (and win (not (window-dedicated-p win))) - (display-buffer-record-window 'reuse win buffer) - (set-window-buffer win buffer) - win))) +non-agent buffer, which is replaced (it stays alive, just unshown). + +Skipped entirely when the prior toggle-off deleted the agent's own split +window (`cj/--ai-term-last-toggle-deleted-split'): re-showing then reuses a +working window at the edge and collapses the layout. Consume the flag and +return nil so `cj/--ai-term-display-saved' re-splits a fresh agent window, +keeping the toggle reversible." + (if cj/--ai-term-last-toggle-deleted-split + (progn (setq cj/--ai-term-last-toggle-deleted-split nil) nil) + (let* ((direction (or cj/--ai-term-last-direction + (cj/--ai-term-default-direction))) + (win (cj/window-at-edge direction))) + (when (and win (not (window-dedicated-p win))) + (display-buffer-record-window 'reuse win buffer) + (set-window-buffer win buffer) + win)))) (defun cj/--ai-term-display-saved (buffer alist) "Display-buffer action: split per saved direction and size. @@ -824,6 +844,7 @@ Two cases, by window count: ((one-window-p) (cj/--ai-term-capture-state win) (setq cj/--ai-term-last-was-bury t) + (setq cj/--ai-term-last-toggle-deleted-split nil) (bury-buffer (window-buffer win)) (when (and (window-live-p win) (cj/--ai-term-buffer-p (window-buffer win))) @@ -833,9 +854,15 @@ Two cases, by window count: (setq cj/--ai-term-last-was-bury nil) (if (and (window-live-p win) (> (length (window-list (window-frame win) 'never)) 1)) - (delete-window win) + (progn + (delete-window win) + ;; The agent had its own window in a multi-window layout, now gone: + ;; the next toggle-on must re-split it rather than reuse a working + ;; window at the edge (see `cj/--ai-term-reuse-edge-window'). + (setq cj/--ai-term-last-toggle-deleted-split t)) ;; Degenerate fallback (window became sole between dispatch and ;; here): swap to a non-agent buffer rather than leave the agent up. + (setq cj/--ai-term-last-toggle-deleted-split nil) (when (window-live-p win) (cj/--ai-term-swap-to-working-buffer win))))) nil) diff --git a/tests/test-ai-term--reuse-edge-window.el b/tests/test-ai-term--reuse-edge-window.el index f6259ae50..a9a0529e8 100644 --- a/tests/test-ai-term--reuse-edge-window.el +++ b/tests/test-ai-term--reuse-edge-window.el @@ -269,5 +269,46 @@ most-recent agent, which would now be the other one." (when (get-buffer right-name) (kill-buffer right-name)) (cj/test--kill-agent-buffers)))) +(ert-deftest test-ai-term--reuse-edge-window-3win-toggle-restores-own-window () + "Regression: in a 3-window layout the agent has its own split, so toggling it +off then on restores it as its own window without displacing a working window. +Before the fix, toggle-on reused the bottom edge (the user's main window), +collapsing three windows to two and hiding the main buffer. A toggle must be +reversible: off then on returns to the same layout." + (cj/test--kill-agent-buffers) + (let ((agent-name "agent [3win-toggle]") + (code-name "*test-3win-code*") + (main-name "*test-3win-main*") + (cj/--ai-term-last-direction nil) + (cj/--ai-term-last-size nil) + (cj/--ai-term-last-was-bury nil)) + (unwind-protect + (save-window-excursion + (delete-other-windows) + (cl-letf (((symbol-function 'cj/--ai-term-default-direction) (lambda (&rest _) 'below))) + (let ((code-buf (get-buffer-create code-name)) + (main-buf (get-buffer-create main-name)) + (agent-buf (get-buffer-create agent-name))) + (set-window-buffer (selected-window) code-buf) + (let* ((main-win (split-window (selected-window) nil 'below)) + (agent-win (split-window main-win nil 'below))) + (set-window-buffer main-win main-buf) + (set-window-buffer agent-win agent-buf) + (should (= (count-windows) 3)) + (let ((display-buffer-alist (cj/--ai-term-display-rule-list))) + (select-window agent-win) + (cj/test--call-as-gui #'cj/ai-term) ; off -> code | main + (should (= (count-windows) 2)) + (should-not (member agent-name (cj/test--displayed-buffer-names))) + (cj/test--call-as-gui #'cj/ai-term) ; on -> back to 3 windows + (should (= (count-windows) 3)) + (let ((bufs (cj/test--displayed-buffer-names))) + (should (member agent-name bufs)) + (should (member code-name bufs)) + (should (member main-name bufs)))))))) + (when (get-buffer code-name) (kill-buffer code-name)) + (when (get-buffer main-name) (kill-buffer main-name)) + (cj/test--kill-agent-buffers)))) + (provide 'test-ai-term--reuse-edge-window) ;;; test-ai-term--reuse-edge-window.el ends here |
