diff options
| author | Craig Jennings <c@cjennings.net> | 2026-07-01 22:42:38 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-07-01 22:42:38 -0400 |
| commit | cf6dfae6ad311991ce0914370c04e60c284874b2 (patch) | |
| tree | a501fc585bde2a2e0f419d80f7d369281cf5a7e5 /modules/ai-term-backend-eat.el | |
| parent | e18cf02e22049ad3cc4ce96059edc37a5ecb6719 (diff) | |
| download | dotemacs-cf6dfae6ad311991ce0914370c04e60c284874b2.tar.gz dotemacs-cf6dfae6ad311991ce0914370c04e60c284874b2.zip | |
refactor(ai-term): split into sessions, display, and EAT-backend layers
ai-term.el had grown to ~1,215 lines mixing project/tmux session discovery, window display policy, the EAT terminal backend, and the public commands, so a change to any one risked coupling to the others.
I extracted three layers, following the calendar-sync split shape: ai-term-sessions.el (discovery, tmux naming and parsing, launch command, picker ordering), ai-term-display.el (display-buffer actions and rule, toggle state, server-window routing), and ai-term-backend-eat.el (terminal create/reattach, pty send, EAT keymap). The backend file is named for its backend so a future one lands as a sibling. ai-term.el stays the public face (options, commands, keymap, shutdown), every name unchanged, so existing (require 'ai-term) callers and all 30 test files work as before.
The extracted layers forward-declare the face's defcustoms rather than requiring it, keeping the graph acyclic. I dropped the unused cl-lib and host-environment requires and added the three modules to the header-contract list.
Diffstat (limited to 'modules/ai-term-backend-eat.el')
| -rw-r--r-- | modules/ai-term-backend-eat.el | 93 |
1 files changed, 93 insertions, 0 deletions
diff --git a/modules/ai-term-backend-eat.el b/modules/ai-term-backend-eat.el new file mode 100644 index 00000000..6d512831 --- /dev/null +++ b/modules/ai-term-backend-eat.el @@ -0,0 +1,93 @@ +;;; ai-term-backend-eat.el --- EAT terminal backend for ai-term -*- lexical-binding: t; -*- + +;; Author: Craig Jennings <c@cjennings.net> + +;;; Commentary: +;; +;; Layer: 3 (Domain Workflow). +;; Category: D. +;; Load shape: library. +;; Top-level side effects: binds M-SPC in `eat-semi-char-mode-map' after EAT +;; loads. +;; Runtime requires: ai-term-sessions. +;; Direct test load: yes. +;; +;; EAT backend of ai-term: terminal buffer creation and tmux reattach, the +;; pty send helper, and the EAT keymap integration. The name is +;; backend-specific on purpose -- a future terminal backend lands as a +;; sibling ai-term-backend-<name>.el and everything above this layer stays +;; put. The display routing the created buffer goes through lives in +;; ai-term-display (the display-buffer-alist rule); this module only calls +;; `display-buffer'. + +;;; Code: + +(require 'ai-term-sessions) + +(declare-function eat "eat" (&optional program arg)) +(declare-function cj/ai-term-next "ai-term" ()) +(defvar eat-buffer-name) +(defvar eat-semi-char-mode-map) + +(defun cj/--ai-term-send-string (buffer string) + "Send STRING to BUFFER's terminal process (the agent's shell). +Sends to the pty directly so the launch command reaches the shell EAT runs." + (let ((proc (get-buffer-process buffer))) + (when (process-live-p proc) + (process-send-string proc string)))) + +(defun cj/--ai-term-show-or-create (dir name) + "Show or create the AI-term buffer for project DIR with buffer NAME. + +If a buffer named NAME exists with a live process, display it. If +the buffer exists but its process is dead, kill it and recreate. If +no such buffer exists, create a new EAT terminal in DIR and send +the project's tmux launch command (see `cj/--ai-term-launch-command') so +the same project basename reattaches across Emacs restarts. + +EAT runs a plain shell with no auto-tmux hook, so the named +`tmux new-session -A' launch command is the only thing that starts the +session -- the spike confirmed EAT + tmux detach and reattach exactly +like ghostel + tmux did. + +Records DIR in `cj/--ai-term-mru' (whichever branch runs) so the +project picker can list recently-opened projects first. Returns the +buffer." + (cj/--ai-term-record-mru dir) + (let ((existing (get-buffer name))) + (cond + ((and existing (cj/--ai-term-process-live-p existing)) + (display-buffer existing) + existing) + (t + (when existing + (kill-buffer existing)) + ;; `eat' switches to its buffer in the selected window before our + ;; display-buffer-alist rule can route it; `save-window-excursion' + ;; reverts that, and the explicit display-buffer below routes the buffer + ;; through the alist into the agent slot. `eat-buffer-name' is bound to + ;; NAME so the terminal is created under the agent name; EAT (unlike + ;; ghostel) does not rename the buffer from the terminal's OSC title, so + ;; the "agent [" prefix that buffer detection and the display rule key on + ;; stays put. + (save-window-excursion + (let ((default-directory dir) + (eat-buffer-name name)) + (eat))) + (let ((buf (get-buffer name))) + (with-current-buffer buf + (cj/--ai-term-send-string + buf (concat (cj/--ai-term-launch-command dir) "\n"))) + (display-buffer buf) + buf))))) + +;; In EAT's semi-char mode, keys not bound in `eat-semi-char-mode-map' are +;; forwarded to the pty. M-SPC (swap to the next agent) must reach Emacs from +;; inside an agent buffer, so bind it in that map -- no exception-list or rebuild +;; dance like ghostel needed. C-; is already bound there (eat-config), so the +;; C-; a family resolves through the global prefix without extra wiring. +(with-eval-after-load 'eat + (keymap-set eat-semi-char-mode-map "M-SPC" #'cj/ai-term-next)) + +(provide 'ai-term-backend-eat) +;;; ai-term-backend-eat.el ends here |
