aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-28 15:29:37 -0400
committerCraig Jennings <c@cjennings.net>2026-06-28 15:29:37 -0400
commit3c54def0bb2637da6b0700d72defa3a3f838909b (patch)
tree5bdb2fe703c79193986703f63063afe685798fd6
parent7cdd9a7490e6a3d0725adcf6fea8c678fafc4416 (diff)
downloaddotemacs-3c54def0bb2637da6b0700d72defa3a3f838909b.tar.gz
dotemacs-3c54def0bb2637da6b0700d72defa3a3f838909b.zip
fix(ai-term): keep agent buffers alive through the kill-all sweep
F1 (cj/dashboard-only) kills every other buffer, burying only those on the undead list. Agent buffers were never registered, so the sweep killed live agents and detached their sessions. Agent buffers are a dynamic family ("agent [<project>]") that an exact-name list can't pre-enumerate, so undead-buffers gains a regexp list (cj/undead-buffer-regexps) and a centralized cj/--buffer-undead-p predicate. Both kill paths route through it. ai-term registers the "agent [" pattern, so every agent -- current or future, however created -- is buried rather than killed.
-rw-r--r--modules/ai-term.el7
-rw-r--r--modules/undead-buffers.el29
-rw-r--r--tests/test-undead-buffers--buffer-undead-p.el52
3 files changed, 85 insertions, 3 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el
index ff240b9bf..6dfb669a9 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -30,6 +30,7 @@
(require 'keybindings) ;; provides cj/register-prefix-map (C-; a)
(declare-function eat "eat" (&optional program arg))
+(declare-function cj/make-buffer-pattern-undead "undead-buffers")
(defvar eat-buffer-name)
(defvar eat-semi-char-mode-map)
@@ -516,6 +517,12 @@ repeated capture/replay drifts the dock height a couple rows per cycle."
(add-hook 'window-configuration-change-hook #'cj/--ai-term-track-geometry)
+;; Agent buffers ("agent [<project>]") are buried, not killed, by the
+;; kill-all sweep (F1 / `cj/dashboard-only'). Register the family pattern so
+;; every agent -- however and whenever created -- survives with its session.
+(with-eval-after-load 'undead-buffers
+ (cj/make-buffer-pattern-undead "\\`agent \\["))
+
(defun cj/--ai-term-reuse-existing-agent (buffer _alist)
"Display-buffer action: reuse any window in this frame already showing
an agent buffer.
diff --git a/modules/undead-buffers.el b/modules/undead-buffers.el
index fe43575e9..4780ef227 100644
--- a/modules/undead-buffers.el
+++ b/modules/undead-buffers.el
@@ -32,7 +32,13 @@
(defvar cj/undead-buffer-list
'("*scratch*" "*EMMS-Playlist*" "*Messages*" "*ert*"
"*AI-Assistant*")
- "Buffers to bury instead of killing.")
+ "Buffer names to bury instead of killing (exact match).")
+
+(defvar cj/undead-buffer-regexps nil
+ "Regexps for buffer names to bury instead of killing, alongside
+`cj/undead-buffer-list'. Use for dynamically-named buffer families where an
+exact name can't be pre-listed -- e.g. ai-term agents, named \"agent [<project>]\".
+Register one with `cj/make-buffer-pattern-undead'.")
(defun cj/make-buffer-undead (name)
"Append NAME to `cj/undead-buffer-list' if not present.
@@ -41,6 +47,23 @@ Signal an error if NAME is not a non-empty string. Return the updated list."
(error "cj/bury-alive-add: NAME must be a non-empty string"))
(add-to-list 'cj/undead-buffer-list name t))
+(defun cj/make-buffer-pattern-undead (regexp)
+ "Append REGEXP to `cj/undead-buffer-regexps' if not present.
+A buffer whose name matches REGEXP is buried instead of killed. Signal an
+error if REGEXP is not a non-empty string. Return the updated list."
+ (unless (and (stringp regexp) (> (length regexp) 0))
+ (error "cj/make-buffer-pattern-undead: REGEXP must be a non-empty string"))
+ (add-to-list 'cj/undead-buffer-regexps regexp t))
+
+(defun cj/--buffer-undead-p (name)
+ "Return non-nil when buffer NAME should be buried instead of killed.
+NAME is undead when it is in `cj/undead-buffer-list' (exact) or matches any
+regexp in `cj/undead-buffer-regexps'."
+ (and (stringp name)
+ (or (member name cj/undead-buffer-list)
+ (seq-some (lambda (re) (string-match-p re name))
+ cj/undead-buffer-regexps))))
+
(defun cj/kill-buffer-or-bury-alive (buffer)
"Kill BUFFER or bury it if it's in `cj/undead-buffer-list'."
(interactive "bBuffer to kill or bury: ")
@@ -49,7 +72,7 @@ Signal an error if NAME is not a non-empty string. Return the updated list."
(progn
(add-to-list 'cj/undead-buffer-list (buffer-name))
(message "Added %s to bury-alive-list" (buffer-name)))
- (if (member (buffer-name) cj/undead-buffer-list)
+ (if (cj/--buffer-undead-p (buffer-name))
(bury-buffer)
(kill-buffer)))))
(keymap-global-set "<remap> <kill-buffer>" #'cj/kill-buffer-or-bury-alive)
@@ -60,7 +83,7 @@ Undead-buffers are buffers in `cj/undead-buffer-list'."
(let* ((buf (current-buffer))
(name (buffer-name buf)))
(and
- (not (member name cj/undead-buffer-list))
+ (not (cj/--buffer-undead-p name))
(buffer-file-name buf)
(buffer-modified-p buf))))
diff --git a/tests/test-undead-buffers--buffer-undead-p.el b/tests/test-undead-buffers--buffer-undead-p.el
new file mode 100644
index 000000000..e196e41a9
--- /dev/null
+++ b/tests/test-undead-buffers--buffer-undead-p.el
@@ -0,0 +1,52 @@
+;;; test-undead-buffers--buffer-undead-p.el --- undead predicate (name + regexp) -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; `cj/--buffer-undead-p' decides whether a buffer name is buried instead of
+;; killed. A name is undead when it is in `cj/undead-buffer-list' (exact) or
+;; matches any regexp in `cj/undead-buffer-regexps' (dynamic families like the
+;; ai-term agent buffers, named "agent [<project>]"). `cj/make-buffer-pattern-undead'
+;; registers a regexp.
+
+;;; Code:
+
+(require 'ert)
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(require 'undead-buffers)
+
+(ert-deftest test-undead-buffer-undead-p-name-list ()
+ "Normal: a name in the exact list is undead; others are not."
+ (let ((cj/undead-buffer-list '("*scratch*" "*Messages*"))
+ (cj/undead-buffer-regexps nil))
+ (should (cj/--buffer-undead-p "*scratch*"))
+ (should (cj/--buffer-undead-p "*Messages*"))
+ (should-not (cj/--buffer-undead-p "other"))))
+
+(ert-deftest test-undead-buffer-undead-p-regexp ()
+ "Normal: a name matching a regexp is undead; the agent pattern is anchored."
+ (let ((cj/undead-buffer-list nil)
+ (cj/undead-buffer-regexps '("\\`agent \\[")))
+ (should (cj/--buffer-undead-p "agent [rulesets]"))
+ (should (cj/--buffer-undead-p "agent [.emacs.d]"))
+ (should-not (cj/--buffer-undead-p "not an agent"))
+ (should-not (cj/--buffer-undead-p "my agent [x]")))) ; anchored: must start with "agent ["
+
+(ert-deftest test-undead-buffer-undead-p-neither ()
+ "Boundary/Error: a name in neither, an empty string, and a non-string are not undead."
+ (let ((cj/undead-buffer-list '("*scratch*"))
+ (cj/undead-buffer-regexps '("\\`agent \\[")))
+ (should-not (cj/--buffer-undead-p "random"))
+ (should-not (cj/--buffer-undead-p ""))
+ (should-not (cj/--buffer-undead-p nil))))
+
+(ert-deftest test-undead-make-buffer-pattern-undead-adds-and-rejects ()
+ "Normal/Error: registering a regexp makes matching names undead; a blank or
+non-string regexp signals."
+ (let ((cj/undead-buffer-regexps nil))
+ (cj/make-buffer-pattern-undead "\\`agent \\[")
+ (should (member "\\`agent \\[" cj/undead-buffer-regexps))
+ (should (cj/--buffer-undead-p "agent [x]"))
+ (should-error (cj/make-buffer-pattern-undead ""))
+ (should-error (cj/make-buffer-pattern-undead 42))))
+
+(provide 'test-undead-buffers--buffer-undead-p)
+;;; test-undead-buffers--buffer-undead-p.el ends here