aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-06-24 06:43:45 -0400
committerCraig Jennings <c@cjennings.net>2026-06-24 06:43:45 -0400
commit4e1401d1499be3d24df78edd97310d200e719e10 (patch)
tree9fe55839f6ca7741eaf45ac9f286f5b1bef49089 /modules
parent69931ef2923363e071cb125661ff2f0ed91e890e (diff)
downloaddotemacs-4e1401d1499be3d24df78edd97310d200e719e10.tar.gz
dotemacs-4e1401d1499be3d24df78edd97310d200e719e10.zip
feat(ai-term): wrap-teardown + shutdown entry points for rulesets
Add the three headless functions the rulesets wrap-it-up workflow calls via emacsclient -e, since this module owns the aiv- session naming, the agent buffer, and the geometry restore. cj/ai-term-quit kills a project's tmux session and agent buffer and restores the layout, idempotent and safe when already gone. cj/ai-term-live-count returns the integer count of live aiv- sessions for the shutdown safety gate. cj/ai-term-shutdown-countdown re-checks that gate, then runs an abort-able run-at-time countdown in the echo area and, uncancelled, runs the shutdown command (a defcustom so tests stub it). Reuses the existing kill/close helpers. 13 ERT tests cover the live-count parsing, the quit kill-and-idempotency, and the gate-abort/cancel/tick logic; the tmux and shutdown side effects are manual. Claude-Session: https://claude.ai/code/session_01BqrdWUo9GcznYX2pZr76gZ
Diffstat (limited to 'modules')
-rw-r--r--modules/ai-term.el89
1 files changed, 89 insertions, 0 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el
index 1c98dd5ee..04ff9f6c7 100644
--- a/modules/ai-term.el
+++ b/modules/ai-term.el
@@ -1049,6 +1049,95 @@ picker and C-; a k closes an agent."
(add-to-list 'ghostel-keymap-exceptions "M-SPC")
(ghostel--rebuild-semi-char-keymap))
+;; ------------------- Wrap-it-up teardown + shutdown -------------------------
+;;
+;; Headless entry points the rulesets wrap-it-up workflow calls via
+;; `emacsclient -e' (its Stop hook ~/.claude/hooks/ai-wrap-teardown.sh). All
+;; three must work with no interactive frame guaranteed. rulesets owns the
+;; workflow + hook that call these; this module owns the aiv- session naming,
+;; the agent buffer, and the geometry restore, so the functions live here.
+;; See docs/design/2026-06-23-wrap-teardown-shutdown-proposal.org (rulesets).
+
+(defcustom cj/ai-term-shutdown-command "sudo shutdown now"
+ "Shell command run when the shutdown countdown completes uncancelled.
+A defcustom so development and tests can stub it instead of powering off
+\(sudo is NOPASSWD on Craig's machines, so the default really shuts down)."
+ :type 'string
+ :group 'cj)
+
+(defun cj/ai-term-quit (&optional project)
+ "Tear down PROJECT's AI-term: kill its tmux session, buffer, and restore layout.
+PROJECT is a project basename (as the rulesets Stop hook passes) or a directory;
+nil means the current project (`default-directory'). Kills the `aiv-<name>'
+tmux session (taking the agent process with it), then, when the agent buffer is
+live, swaps its window back to the working buffer and kills it. Idempotent and
+safe headless: a session or buffer already gone is a no-op, not an error."
+ (let* ((key (or project default-directory))
+ (session (cj/--ai-term-tmux-session-name key))
+ (buffer (get-buffer (cj/--ai-term-buffer-name key))))
+ (cj/--ai-term-kill-tmux-session session)
+ (when (cj/--ai-term-buffer-p buffer)
+ (let ((win (get-buffer-window buffer)))
+ (when (window-live-p win)
+ (cj/--ai-term-swap-to-working-buffer win)))
+ (let ((kill-buffer-query-functions nil))
+ (kill-buffer buffer)))
+ session))
+
+(defun cj/ai-term-live-count ()
+ "Return the integer count of live AI-term (aiv-*) tmux sessions.
+0 when tmux has no server or no AI-term sessions. The shutdown safety gate:
+`emacsclient -e (cj/ai-term-live-count)' prints the integer for the hook."
+ (length (cj/--ai-term-live-tmux-sessions)))
+
+(defvar cj/--ai-term-shutdown-timer nil
+ "The active shutdown-countdown repeating timer, or nil when none is running.")
+
+(defun cj/--ai-term-shutdown-clear-timer ()
+ "Cancel and forget the shutdown-countdown timer, if any."
+ (when (timerp cj/--ai-term-shutdown-timer)
+ (cancel-timer cj/--ai-term-shutdown-timer))
+ (setq cj/--ai-term-shutdown-timer nil))
+
+(defun cj/ai-term-shutdown-cancel ()
+ "Cancel an in-progress AI-term shutdown countdown."
+ (interactive)
+ (when cj/--ai-term-shutdown-timer
+ (cj/--ai-term-shutdown-clear-timer)
+ (message "Shutdown cancelled.")))
+
+(defun cj/ai-term-shutdown-countdown (&optional seconds)
+ "Count down SECONDS (default 10) in the echo area, then shut the machine down.
+Re-checks the safety gate first (a TOCTOU guard against the workflow's earlier
+check): aborts with a message when more than one `aiv-*' session is live. The
+countdown is an abort-able `run-at-time' timer -- `C-g' (while the countdown
+owns the keymap) or \\[cj/ai-term-shutdown-cancel] stops it. On reaching zero
+uncancelled it runs `cj/ai-term-shutdown-command'. Returns immediately so the
+Stop hook does not block; the daemon ticks the timer asynchronously."
+ (if (> (cj/ai-term-live-count) 1)
+ (progn
+ (message "Shutdown aborted: %d AI-term sessions still live."
+ (cj/ai-term-live-count))
+ nil)
+ (cj/--ai-term-shutdown-clear-timer)
+ (let ((remaining (or seconds 10)))
+ (set-transient-map
+ (let ((m (make-sparse-keymap)))
+ (define-key m (kbd "C-g") #'cj/ai-term-shutdown-cancel)
+ m)
+ (lambda () (and cj/--ai-term-shutdown-timer t)))
+ (setq cj/--ai-term-shutdown-timer
+ (run-at-time
+ 0 1
+ (lambda ()
+ (if (<= remaining 0)
+ (progn
+ (cj/--ai-term-shutdown-clear-timer)
+ (shell-command cj/ai-term-shutdown-command))
+ (message "Shutting down in %d… (C-g to cancel)" remaining)
+ (setq remaining (1- remaining))))))
+ nil)))
+
;; ---------- emacsclient: keep opened files off the agent terminal ----------
;;
;; `server-start' (in system-defaults.el) leaves `server-window' nil, so