diff options
Diffstat (limited to 'modules/ai-term.el')
| -rw-r--r-- | modules/ai-term.el | 103 |
1 files changed, 66 insertions, 37 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el index 04ff9f6c7..b463da90b 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -52,12 +52,14 @@ ;; 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" +;; - s-F9 `cj/ai-term-next' -- step to the next active agent in the +;; queue. The queue is every active agent in buffer-name order +;; (a stable rotation): attached agents (a live buffer) and +;; detached ones (a live tmux session with no Emacs buffer). +;; Stepping onto a detached agent attaches it. 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 @@ -186,20 +188,39 @@ 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. +(defun cj/--ai-term-next-agent-dir (current dirs) + "Return the project dir after CURRENT in DIRS, 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. +DIRS is an ordered list of active-agent project dirs. When CURRENT is +the last element, wrap to the first. When CURRENT is nil or not a member +of DIRS, return the first dir. Returns nil when DIRS is empty. Matches +with `member' (string equality) since dirs are paths. 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)))) +order driving `cj/ai-term-next' is exercisable in tests." + (when dirs + (if (member current dirs) + (or (cadr (member current dirs)) + (car dirs)) + (car dirs)))) + +(defun cj/--ai-term-active-agent-dirs () + "Return project dirs that have a live agent buffer or a live tmux session. + +Sorted by the agent buffer name, so the rotation is stable and matches +what the picker shows. This is the queue `cj/ai-term-next' steps through: +it includes detached sessions (alive in tmux but with no Emacs buffer), +which the step materializes by attaching." + (let* ((sessions (cj/--ai-term-live-tmux-sessions)) + (live-names (mapcar #'buffer-name (cj/--ai-term-agent-buffers)))) + (sort + (seq-filter + (lambda (dir) + (or (member (cj/--ai-term-buffer-name dir) live-names) + (cj/--ai-term-session-active-p dir sessions))) + (cj/--ai-term-candidates)) + (lambda (a b) + (string< (cj/--ai-term-buffer-name a) (cj/--ai-term-buffer-name b)))))) (defun cj/--ai-term-most-recent-non-agent-buffer () "Return the most-recently-selected live non-agent buffer, or nil. @@ -988,35 +1009,43 @@ interrupt work in progress. Bound to M-<f9>." (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. When none are open, open the project picker -to launch the first agent rather than erroring. +The queue is every active agent ordered by buffer name -- a stable +rotation, unaffected by which agent was most recently selected. Active +means a live agent buffer (attached) OR a live tmux session with no Emacs +buffer (detached); stepping onto a detached agent attaches it (recreates +its terminal, which reattaches the session). When an agent window is on +screen, swap it to the next agent (wrapping after the last) and select it. +When no agent is displayed but agents exist, show the first. When none +are open, open the project picker to launch the first agent rather than +erroring. Bound to M-SPC. Unlike C-; a a (toggle the most-recent agent on/off), this is the \"switch among existing agents\" surface; C-; a s opens the project picker and C-; a k closes an agent." (interactive) - (let* ((buffers (sort (cj/--ai-term-agent-buffers) - (lambda (a b) - (string< (buffer-name a) (buffer-name b))))) + (let* ((dirs (cj/--ai-term-active-agent-dirs)) (win (cj/--ai-term-displayed-agent-window)) - (current (and win (window-buffer win))) - (next (cj/--ai-term-next-agent-buffer current buffers))) - (if (not next) + (current-name (and win (buffer-name (window-buffer win)))) + (current-dir (and current-name + (seq-find (lambda (d) + (equal (cj/--ai-term-buffer-name d) current-name)) + dirs))) + (next-dir (cj/--ai-term-next-agent-dir current-dir dirs))) + (if (not next-dir) ;; No agents open: launch the first via the project picker instead of ;; erroring, so the swap key doubles as a "start an agent" key. (cj/ai-term-pick-project) - (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))))) + (let* ((name (cj/--ai-term-buffer-name next-dir)) + (existing (get-buffer name))) + ;; Live agent and an agent window is up: swap it into that window in + ;; place (faithful to the prior buffer-only behavior). Detached, or no + ;; window yet: show-or-create attaches the tmux session / displays it. + (if (and win existing (cj/--ai-term-process-live-p existing)) + (progn (set-window-buffer win existing) (select-window win)) + (cj/--ai-term-show-or-create next-dir name) + (let ((w (get-buffer-window name))) + (when w (select-window w)))) + (message "Agent: %s" name))))) ;; ai-term lives under the C-; a prefix (vacated when gptel was archived). ;; The frequent "swap to the next agent" also gets M-SPC for a fast chord. |
