From 93c16699304a9349f9678252b70cbc7efd2a4a1f Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sat, 20 Jun 2026 22:04:24 -0400 Subject: feat(ai-term): add s-F9 step-to-next-agent, drop C-S-F9 close alias s-F9 (cj/ai-term-next) steps through the open agent buffers in name order. It's the "switch among existing agents" surface F9's toggle never provided. The cycle logic lives in a pure helper (cj/--ai-term-next-agent-buffer) with Normal/Boundary/Error coverage. The command is a thin window-mutating wrapper. I dropped the C-S-F9 close alias, leaving M-F9 as the sole close binding. I moved cj/server-shutdown off C- to C-x C so the key keeps forwarding to the terminal program inside an agent buffer. I also removed the now-unused F10 entries from term-config's ghostel exceptions. --- modules/ai-term.el | 67 +++++++++++++++++++++++++++++++++++++++++++------ modules/system-utils.el | 2 +- modules/term-config.el | 11 ++++---- 3 files changed, 66 insertions(+), 14 deletions(-) (limited to 'modules') diff --git a/modules/ai-term.el b/modules/ai-term.el index 25e56c508..ff8da0035 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -52,15 +52,19 @@ ;; picker, even when an agent buffer is currently displayed. ;; Used when the user wants to start a new project session ;; instead of toggling the current one. +;; - s-F9 `cj/ai-term-next' -- step to the next open agent in the +;; queue. The queue is the live agent buffers in buffer-name +;; order (a stable rotation). When an agent window is on +;; screen, swap it to the next agent and focus it, wrapping +;; after the last; when none is shown but agents exist, show +;; the first. This is the "switch among existing agents" +;; surface F9 deliberately doesn't provide. ;; - M-F9 `cj/ai-term-close' -- gracefully close an agent: kill its ;; tmux session (stopping the agent process), then its terminal ;; buffer. Its window stays in the layout (swapped to the ;; working buffer), so closing never collapses a split. Confirms ;; first. Targets the current agent, the sole live agent, or ;; prompts among several. -;; - C-S-F9 `cj/ai-term-close' -- same close command, second binding. -;; (M-F9 is the primary; C-S-F9 may be swallowed by the -;; Wayland/PGTK layer on some machines.) ;; ;; Existing windmove (Shift-arrows) handles code <-> agent focus ;; toggling. Buffer-move (C-M-arrows) handles side-swap. Neither @@ -181,6 +185,21 @@ recently-selected first. Non-AI-term buffers are filtered out via `cj/--ai-term-buffer-p'." (seq-filter #'cj/--ai-term-buffer-p (buffer-list))) +(defun cj/--ai-term-next-agent-buffer (current buffers) + "Return the agent buffer after CURRENT in BUFFERS, wrapping to the first. + +BUFFERS is an ordered list of live agent buffers. When CURRENT is the +last element, wrap to the first. When CURRENT is nil or not a member of +BUFFERS, return the first buffer. Returns nil when BUFFERS is empty. + +Pure decision helper (no buffer or window side effects) so the cycle +order driving `cj/ai-term-next' (s-F9) is exercisable in tests." + (when buffers + (if (memq current buffers) + (or (cadr (memq current buffers)) + (car buffers)) + (car buffers)))) + (defun cj/--ai-term-most-recent-non-agent-buffer () "Return the most-recently-selected live non-agent buffer, or nil. @@ -882,7 +901,7 @@ With prefix ARG, display the buffer without selecting its window when a buffer is being shown (no effect on the toggle-off branch). See `cj/ai-term-pick-project' (C-F9) to force the project picker. -M-F9 (and C-S-F9) close an agent via `cj/ai-term-close'." +M-F9 closes an agent via `cj/ai-term-close'." (interactive "P") (pcase (cj/--ai-term-dispatch) (`(toggle-off . ,win) @@ -952,7 +971,7 @@ buffers; nil when none are alive." Targets the current agent buffer, the sole live agent, or prompts when several are alive (see `cj/--ai-term-close-target'). Asks for confirmation first -- this kills the running agent process, which can -interrupt work in progress. Bound to M- (primary) and C-S-." +interrupt work in progress. Bound to M-." (interactive) (let ((buffer (cj/--ai-term-close-target))) (unless buffer @@ -963,10 +982,42 @@ interrupt work in progress. Bound to M- (primary) and C-S-." (cj/--ai-term-close-buffer buffer) (message "Closed agent %s." name))))) +;; ------------------------- Step to the next agent ---------------------------- + +(defun cj/ai-term-next () + "Step to the next open AI-term agent in the queue. + +The queue is the live agent buffers ordered by buffer name -- a stable +rotation, unaffected by which agent was most recently selected. When an +agent window is on screen, swap it to the next agent in the queue +\(wrapping after the last) and select it. When no agent is displayed but +agents exist, show the first. Signals `user-error' when none are open. + +Bound to s-. Unlike (toggle the most-recent agent on/off), this +is the \"switch among existing agents\" surface; C- opens the project +picker and M- closes an agent." + (interactive) + (let* ((buffers (sort (cj/--ai-term-agent-buffers) + (lambda (a b) + (string< (buffer-name a) (buffer-name b))))) + (win (cj/--ai-term-displayed-agent-window)) + (current (and win (window-buffer win))) + (next (cj/--ai-term-next-agent-buffer current buffers))) + (unless next + (user-error "No AI-term agent buffers open")) + (if win + (progn + (set-window-buffer win next) + (select-window win)) + (display-buffer next) + (let ((w (get-buffer-window next))) + (when w (select-window w)))) + (message "Agent: %s" (buffer-name next)))) + (keymap-global-set "" #'cj/ai-term) (keymap-global-set "C-" #'cj/ai-term-pick-project) +(keymap-global-set "s-" #'cj/ai-term-next) (keymap-global-set "M-" #'cj/ai-term-close) -(keymap-global-set "C-S-" #'cj/ai-term-close) ;; ghostel's semi-char mode forwards keys not in `ghostel-keymap-exceptions' to ;; the terminal program, so a plain typed while point is inside an agent @@ -977,15 +1028,15 @@ interrupt work in progress. Bound to M- (primary) and C-S-." (with-eval-after-load 'ghostel (keymap-set ghostel-mode-map "" #'cj/ai-term) (keymap-set ghostel-mode-map "C-" #'cj/ai-term-pick-project) + (keymap-set ghostel-mode-map "s-" #'cj/ai-term-next) (keymap-set ghostel-mode-map "M-" #'cj/ai-term-close) - (keymap-set ghostel-mode-map "C-S-" #'cj/ai-term-close) ;; The bindings above live in `ghostel-mode-map', but in semi-char mode ;; ghostel's own `ghostel-semi-char-mode-map' forwards every key not in ;; `ghostel-keymap-exceptions' to the pty -- and that map outranks the ;; major-mode map, so it would swallow the F9 family before the bindings ;; above fire. Add the family to the exceptions and rebuild the semi-char ;; map so the keys fall through to `ghostel-mode-map' inside agent buffers. - (dolist (key '("" "C-" "M-" "C-S-")) + (dolist (key '("" "C-" "s-" "M-")) (add-to-list 'ghostel-keymap-exceptions key)) (ghostel--rebuild-semi-char-keymap)) diff --git a/modules/system-utils.el b/modules/system-utils.el index 7cf958674..254a2f502 100644 --- a/modules/system-utils.el +++ b/modules/system-utils.el @@ -102,7 +102,7 @@ detached from Emacs." (interactive) (save-some-buffers) (kill-emacs)) -(keymap-global-set "C-" #'cj/server-shutdown) +(keymap-global-set "C-x C" #'cj/server-shutdown) ;;; ---------------------------- History Persistence ---------------------------- diff --git a/modules/term-config.el b/modules/term-config.el index 0a7991409..c1c28911d 100644 --- a/modules/term-config.el +++ b/modules/term-config.el @@ -246,12 +246,13 @@ run its own project-named tmux session instead of a bare, auto-named one. ;; rebuild is what actually lets the key through to `ghostel-mode-map' / the ;; global map. C-; and F12 are the prefix + toggle; the modified arrows are ;; windmove (S-arrows, focus) and buffer-move (C-M-arrows, swap), which the - ;; ai-term workflow expects to work from inside an agent buffer. F8, F10 and - ;; C-F10 are global bindings (org agenda, music-playlist toggle, server - ;; shutdown) that reach Emacs by falling through to the global map once the - ;; semi-char map stops forwarding them. + ;; ai-term workflow expects to work from inside an agent buffer. F8 and F10 + ;; are global bindings (org agenda, music-playlist toggle) that reach Emacs by + ;; falling through to the global map once the semi-char map stops forwarding + ;; them. (Server shutdown moved off C-F10 to C-x C, which is deliberately + ;; left forwarding to the terminal program inside an agent buffer.) (with-eval-after-load 'ghostel - (dolist (key '("C-;" "" "" "" "C-" + (dolist (key '("C-;" "" "" "" "S-" "S-" "S-" "S-" "C-M-" "C-M-" "C-M-" "C-M-")) (add-to-list 'ghostel-keymap-exceptions key)) -- cgit v1.2.3