diff options
Diffstat (limited to 'modules/ai-term.el')
| -rw-r--r-- | modules/ai-term.el | 124 |
1 files changed, 36 insertions, 88 deletions
diff --git a/modules/ai-term.el b/modules/ai-term.el index 03c9a4046..4b2495715 100644 --- a/modules/ai-term.el +++ b/modules/ai-term.el @@ -1,4 +1,4 @@ -;;; ai-term.el --- In-Emacs AI-agent launcher with vertical-split terminal -*- lexical-binding: t; -*- +;;; ai-term.el --- AI-agent terminals backed by EAT and tmux -*- lexical-binding: t; -*- ;; Author: Craig Jennings <c@cjennings.net> @@ -7,70 +7,18 @@ ;; Layer: 3 (Domain Workflow). ;; Category: D. ;; Load shape: eager. -;; Eager reason: registers four global keys for the AI-agent terminal launcher; a -;; command-loaded deferral candidate. -;; Top-level side effects: four global key bindings. -;; Runtime requires: cl-lib, seq, cj-window-geometry-lib, cj-window-toggle-lib, -;; host-environment. +;; Eager reason: binds M-SPC and the C-; a AI-agent prefix. +;; Top-level side effects: global M-SPC binding and C-; a prefix map. +;; Runtime requires: cl-lib, seq, window-toggle/geometry helpers, host-environment. ;; Direct test load: yes. ;; -;; Picks an AI-agent project (a dir under ~/.emacs.d, ~/code/*, or -;; ~/projects/* containing .ai/protocols.org), opens or reuses a terminal -;; buffer named "agent [<basename>]", sends the agent's startup -;; instruction to it, and routes the buffer to a side window via -;; display-buffer-alist. When the frame already has a window forming the -;; half the agent would occupy (a right column on a desktop, a bottom row -;; on a laptop), the agent reuses that slot rather than splitting a third -;; window in; toggling off restores the displaced buffer to the slot. -;; Otherwise placement is a host-aware split: a right-side split at 50% -;; width on a desktop, a bottom split at 75% height on a laptop (see -;; `cj/--ai-term-default-direction'). Multiple -;; projects produce multiple coexisting buffers that share the same -;; slot; switching among them is a buffer-switch, not a -;; kill-and-recreate. +;; Opens project-scoped AI agents in EAT buffers backed by tmux sessions. Project +;; candidates come from configured roots that contain .ai/protocols.org. ;; -;; Each project's agent runs inside a tmux session named -;; "<cj/ai-term-tmux-session-prefix><basename>" (default prefix "aiv-"). -;; The prefix lets `tmux ls' be filtered to AI-term's own sessions, so -;; after an Emacs crash the project picker can match surviving sessions -;; back to their directories: matched projects sort to the top of the -;; picker (flagged "[detached]" -- session alive, no Emacs buffer -- or -;; "[running]" when a live terminal buffer exists), the rest follow in -;; alphabetical order. -;; -;; Four F-key entry points: -;; -;; - F9 `cj/ai-term' -- DWIM dispatch. If an agent buffer is -;; currently displayed in this frame, F9 toggles it off: when it -;; took over an existing window (a reused slot) the buffer it -;; displaced returns to that slot, when it was split into its own -;; window that window is removed, and when it fills the frame it -;; is buried. Otherwise, if exactly one agent buffer is alive, -;; F9 re-displays it; if zero or two-plus are alive, F9 falls -;; through to the project picker. -;; - C-F9 `cj/ai-term-pick-project' -- always show the project -;; 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 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 -;; 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. -;; -;; Existing windmove (Shift-arrows) handles code <-> agent focus -;; toggling. Buffer-move (C-M-arrows) handles side-swap. Neither -;; needs anything new from this module. +;; Agent display reuses the host-appropriate side slot when possible, otherwise +;; splits right on desktop frames and below on laptop frames. Attached buffers +;; and detached tmux sessions share the same rotation; selecting a detached +;; agent recreates its EAT buffer and attaches to the live session. ;;; Code: @@ -215,7 +163,7 @@ which the step materializes by attaching." Walks `buffer-list' (most-recently-selected first) and returns the first buffer that is not an AI-term agent buffer (per `cj/--ai-term-buffer-p') and is not an internal buffer (name starting -with a space). Used by the single-window F9 toggle-off so dismissing a +with a space). Used by the single-window toggle-off so dismissing a full-frame agent returns to the file the user was working in (e.g. todo.org) rather than swapping in another agent." (seq-find (lambda (b) @@ -287,7 +235,7 @@ looked up in SESSIONS, so the lossy whitespace->hyphen transform in (defun cj/--ai-term-launch-command (dir) "Return the shell command line that runs the AI tool in a project tmux session. -Uses `tmux new-session -A' so a second F9 on the same project reattaches +Uses `tmux new-session -A' so a second toggle on the same project reattaches to the running session instead of spawning a new one. The session name comes from `cj/--ai-term-tmux-session-name'; the first window is named `cj/ai-term-tmux-window-name' (default \"ai\") so a later hand-opened @@ -452,7 +400,7 @@ direction applies. Captured at toggle-off by `cj/--ai-term-display-saved'.") (defvar cj/--ai-term-last-was-bury nil - "Non-nil when the last F9 toggle-off used `bury-buffer'. + "Non-nil when the last toggle-off used `bury-buffer'. Set by `cj/ai-term' in its `toggle-off' branch: t when the agent window was the only window in the frame (so toggle-off buried @@ -462,7 +410,7 @@ 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. + "Non-nil when the last 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); @@ -474,7 +422,7 @@ 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. + "The agent buffer hidden by the most recent toggle-off. Captured in `cj/ai-term' just before an agent window is torn down, and consumed by `cj/--ai-term-dispatch' so the next toggle-on reopens the @@ -520,7 +468,7 @@ cost of not auto-scaling if the frame itself resizes.") "Capture WINDOW's direction and size into module-level state. Sets `cj/--ai-term-last-direction' and `cj/--ai-term-last-size' -so a subsequent F9 display can restore the user's chosen orientation +so a subsequent display can restore the user's chosen orientation and size. Called at toggle-off (just before the window is torn down). The default direction is host-aware via `cj/--ai-term-default-direction' (used only when WINDOW fills its @@ -544,7 +492,7 @@ action in the chain runs. This is more specific than `display-buffer-use-some-window', which would happily steal any non-selected window (e.g. a code window above the agent split) when the user is focused in agent and -swaps projects via C-F9. The selective lookup here keeps non-agent +swaps projects via C-; a s. The selective lookup here keeps non-agent windows undisturbed and preserves the user's split geometry across project changes." (let ((win (cj/--ai-term-displayed-agent-window))) @@ -598,10 +546,10 @@ When the prior toggle-off was a bury (single-window state, flagged via `cj/--ai-term-last-was-bury') and the frame is still single- window, restore the agent into the selected window in place rather than splitting -- preserves the user's lone-window layout across -F9 toggles. +toggles. Otherwise delegates to `cj/window-toggle-display-saved' against the -F9 state vars, falling back to the host-aware defaults from +toggle state vars, falling back to the host-aware defaults from `cj/--ai-term-default-direction' and `cj/--ai-term-default-size'." (cond ((and cj/--ai-term-last-was-bury (one-window-p)) @@ -627,7 +575,7 @@ through four actions in order: 2. `cj/--ai-term-reuse-existing-agent' -- otherwise, if any window in this frame already shows an agent-prefixed buffer, swap its buffer for the new one (preserves geometry across - project changes via C-F9). + project changes via C-; a s). 3. `cj/--ai-term-reuse-edge-window' -- otherwise, if the frame already has a window forming the half the agent would occupy (the right column on a desktop, the bottom row on a laptop), @@ -773,17 +721,17 @@ Signals `user-error' when no candidates exist." (expand-file-name chosen))))) (defun cj/--ai-term-dispatch () - "Compute the F9 (`cj/ai-term') action without performing it. + "Compute the `cj/ai-term' (C-; a a) action without performing it. Returns one of: - (toggle-off . WINDOW) -- agent is displayed in WINDOW; quit it. - (redisplay-recent . BUFFER) -- 1+ alive agent buffers; show MRU. - (pick-project) -- zero alive agent buffers; prompt. -When 2+ agent buffers are alive, F9 redisplays the most-recently- -selected one rather than opening the project picker. C-F9 is the -explicit \"start a different project\" surface; M-F9 is the explicit -\"switch among existing agents\" surface. F9 keeps a single, simple +When 2+ agent buffers are alive, C-; a a redisplays the most-recently- +selected one rather than opening the project picker. C-; a s is the +explicit \"start a different project\" surface; C-; a n is the explicit +\"switch among existing agents\" surface. C-; a a keeps a single, simple job: toggle whichever agent was last in use. A pure-decision helper so the dispatch logic is exercisable in tests @@ -816,7 +764,7 @@ buffers; reinvoking on the same project reuses its existing terminal. With prefix ARG, display the buffer without selecting its window. -Bound to C-F9 -- always shows the project picker, even when an agent +Bound to C-; a s -- always shows the project picker, even when an agent buffer is currently displayed. EAT renders in terminal frames as well as GUI frames, so this @@ -842,7 +790,7 @@ the agent itself." (other-buffer (window-buffer win) t))))) (defun cj/--ai-term-toggle-off (win) - "Hide the agent shown in WIN for an F9 toggle-off. Always returns nil. + "Hide the agent shown in WIN for a toggle-off. Always returns nil. Two cases, by window count: @@ -855,7 +803,7 @@ Two cases, by window count: force a swap to a non-agent buffer to keep the toggle observable. - Multi-window: collapse the agent split outright by deleting its window, so - the working buffer (e.g. todo.org) reclaims the space. F9 is a pure + the working buffer (e.g. todo.org) reclaims the space. The toggle is a pure show/hide toggle of THE agent split -- it must never surface a different agent. `quit-restore-window' can't guarantee that here: switching among several agents reuses the one slot via `set-window-buffer' (see @@ -897,21 +845,21 @@ Two cases, by window count: nil) (defun cj/ai-term (&optional arg) - "Smart F9 dispatch for the AI-term launcher. + "DWIM dispatch for the AI-term launcher. Bound to C-; a a. Behavior depends on the current state: -- If an AI-term buffer is currently displayed in this frame, F9 +- If an AI-term buffer is currently displayed in this frame, it quits its window (toggle off, buffer stays alive). -- Else, if exactly one alive AI-term buffer exists, F9 re-displays +- Else, if exactly one alive AI-term buffer exists, it re-displays it (DWIM -- the obvious next step is to look at it). -- Else (zero or 2+), F9 falls through to `cj/ai-term-pick-project'. +- Else (zero or 2+), it falls through to `cj/ai-term-pick-project'. 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 closes an agent via `cj/ai-term-close'." +See `cj/ai-term-pick-project' (C-; a s) to force the project picker. +C-; a k closes an agent via `cj/ai-term-close'." (interactive "P") (pcase (cj/--ai-term-dispatch) (`(toggle-off . ,win) @@ -945,7 +893,7 @@ Derives the tmux session name from BUFFER's `default-directory' (the project dir the terminal was created in) and kills it so the agent process stops. When BUFFER is shown, swaps its window to a non-agent buffer (the working file) rather than deleting the window -- closing an -agent must not collapse the user's window layout; the F9 hide toggle is +agent must not collapse the user's window layout; the hide toggle is what collapses the split. Then kills BUFFER (suppressing the process-still-running prompt -- the session is already down). No-op when BUFFER isn't an AI-term buffer." @@ -984,7 +932,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-<f9>." +interrupt work in progress. Bound to C-; a k." (interactive) (let ((buffer (cj/--ai-term-close-target))) (unless buffer |
