aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/ai-vterm.el16
-rw-r--r--tests/test-ai-vterm--display-saved.el4
-rw-r--r--tests/test-ai-vterm--reuse-edge-window.el10
-rw-r--r--tests/test-ai-vterm--single-window-toggle.el12
-rw-r--r--tests/test-ai-vterm--terminal-guard.el78
-rw-r--r--tests/testutil-vterm-buffers.el13
-rw-r--r--todo.org8
7 files changed, 128 insertions, 13 deletions
diff --git a/modules/ai-vterm.el b/modules/ai-vterm.el
index 70395ecc..00589890 100644
--- a/modules/ai-vterm.el
+++ b/modules/ai-vterm.el
@@ -698,6 +698,19 @@ without firing real `display-buffer' or `quit-window' calls."
(buffers (cons 'redisplay-recent (car buffers)))
(t '(pick-project))))))))
+(defun cj/--ai-vterm-refuse-in-terminal ()
+ "Signal a `user-error' when the current frame is a terminal frame.
+
+AI-vterm launches a graphical vterm side window, so it is GUI-only.
+Each interactive entry point calls this first, so F9 and friends
+decline -- with a message in the echo area -- in a terminal frame
+instead of launching a vterm. The check is per-frame at command time
+rather than at load, so a daemon serving both GUI and terminal frames
+keeps the launcher working in its GUI frames and declines only in the
+terminal ones."
+ (when (env-terminal-p)
+ (user-error "AI-vterm is GUI-only; not available in a terminal frame")))
+
(defun cj/ai-vterm-pick-project (&optional arg)
"Pick an AI-agent project and open or reuse its vterm.
@@ -712,6 +725,7 @@ With prefix ARG, display the buffer without selecting its window.
Bound to C-F9 -- always shows the project picker, even when an agent
buffer is currently displayed."
(interactive "P")
+ (cj/--ai-vterm-refuse-in-terminal)
(let* ((dir (cj/--ai-vterm-pick-project))
(name (cj/--ai-vterm-buffer-name dir))
(buf (cj/--ai-vterm-show-or-create dir name)))
@@ -737,6 +751,7 @@ when a buffer is being shown (no effect on the toggle-off branch).
See `cj/ai-vterm-pick-project' (C-F9) to force the project picker.
M-F9 (and C-S-F9) close an agent via `cj/ai-vterm-close'."
(interactive "P")
+ (cj/--ai-vterm-refuse-in-terminal)
(pcase (cj/--ai-vterm-dispatch)
(`(toggle-off . ,win)
(cond
@@ -842,6 +857,7 @@ several are alive (see `cj/--ai-vterm-close-target'). Asks for
confirmation first -- this kills the running agent process, which can
interrupt work in progress. Bound to M-<f9> (primary) and C-S-<f9>."
(interactive)
+ (cj/--ai-vterm-refuse-in-terminal)
(let ((buffer (cj/--ai-vterm-close-target)))
(unless buffer
(user-error "No AI-vterm agent buffers to close"))
diff --git a/tests/test-ai-vterm--display-saved.el b/tests/test-ai-vterm--display-saved.el
index 866ff11d..0cf59a29 100644
--- a/tests/test-ai-vterm--display-saved.el
+++ b/tests/test-ai-vterm--display-saved.el
@@ -155,8 +155,8 @@ once and no spurious extra window leaks."
(let ((display-buffer-alist (cj/--ai-vterm-display-rule-list))
(window-count-before (count-windows)))
(select-window agent-win)
- (cj/ai-vterm) ; off
- (cj/ai-vterm) ; on
+ (cj/test--call-as-gui #'cj/ai-vterm) ; off
+ (cj/test--call-as-gui #'cj/ai-vterm) ; on
(should (<= (count-windows) window-count-before))
;; Agent must be displayed exactly once.
(let ((agent-windows
diff --git a/tests/test-ai-vterm--reuse-edge-window.el b/tests/test-ai-vterm--reuse-edge-window.el
index a7009423..9f621477 100644
--- a/tests/test-ai-vterm--reuse-edge-window.el
+++ b/tests/test-ai-vterm--reuse-edge-window.el
@@ -175,7 +175,7 @@ window count stays 2 (the native `quit-restore-window' puts 2 back)."
(should (= (count-windows) 2))
(should (member agent-name (cj/test--displayed-buffer-names)))
;; Toggle off -> the displaced buffer (2) returns to the slot.
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (= (count-windows) 2))
(let ((bufs (cj/test--displayed-buffer-names)))
(should (member right-name bufs))
@@ -213,11 +213,11 @@ the same width."
(display-buffer agent-buf)
(should (= (count-windows) 2))
;; off
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (= (count-windows) 2))
(should-not (cj/--ai-vterm-displayed-agent-window))
;; on again
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (= (count-windows) 2))
(let ((win (cj/--ai-vterm-displayed-agent-window)))
(should (windowp win))
@@ -259,9 +259,9 @@ most-recent agent, which would now be the other one."
(display-buffer a2) ; | left | A2 |
(should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window))
a2))
- (cj/ai-vterm) ; off -> | left | right |
+ (cj/test--call-as-gui #'cj/ai-vterm) ; off -> | left | right |
(should-not (cj/--ai-vterm-displayed-agent-window))
- (cj/ai-vterm) ; on -> must bring A2 back
+ (cj/test--call-as-gui #'cj/ai-vterm) ; on -> must bring A2 back
(should (eq (window-buffer (cj/--ai-vterm-displayed-agent-window))
a2))))))
(when (get-buffer left-name) (kill-buffer left-name))
diff --git a/tests/test-ai-vterm--single-window-toggle.el b/tests/test-ai-vterm--single-window-toggle.el
index 50f1504a..928656f2 100644
--- a/tests/test-ai-vterm--single-window-toggle.el
+++ b/tests/test-ai-vterm--single-window-toggle.el
@@ -48,12 +48,12 @@ batch use both."
(let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)))
;; Toggle off -- the dispatcher's force-swap should put the
;; window on a non-agent buffer.
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (one-window-p))
(should-not (cj/--ai-vterm-displayed-agent-window))
(should (eq cj/--ai-vterm-last-was-bury t))
;; Toggle on -- should restore agent in the same lone window.
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (one-window-p))
(let ((win (cj/--ai-vterm-displayed-agent-window)))
(should (windowp win))
@@ -80,7 +80,7 @@ agent-window state and can route through the display-saved path."
(win (selected-window)))
(set-window-buffer win agent-buf)
(let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)))
- (cj/ai-vterm))
+ (cj/test--call-as-gui #'cj/ai-vterm))
(should (window-live-p win))
(should-not (cj/--ai-vterm-buffer-p (window-buffer win)))))
(cj/test--kill-agent-buffers))))
@@ -96,7 +96,7 @@ agent-window state and can route through the display-saved path."
(let ((agent-buf (get-buffer-create agent-name)))
(set-window-buffer (selected-window) agent-buf)
(let ((display-buffer-alist (cj/--ai-vterm-display-rule-list)))
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should (eq cj/--ai-vterm-last-was-bury t)))))
(cj/test--kill-agent-buffers))))
@@ -119,7 +119,7 @@ toggle-off."
(display-buffer-alist (cj/--ai-vterm-display-rule-list)))
(set-window-buffer agent-win agent-buf)
(select-window agent-win)
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should-not cj/--ai-vterm-last-was-bury))))
(when (get-buffer left-name) (kill-buffer left-name))
(cj/test--kill-agent-buffers))))
@@ -176,7 +176,7 @@ the flag nil (no spurious set)."
(display-buffer-alist (cj/--ai-vterm-display-rule-list)))
(set-window-buffer agent-win agent-buf)
(select-window agent-win)
- (cj/ai-vterm)
+ (cj/test--call-as-gui #'cj/ai-vterm)
(should-not cj/--ai-vterm-last-was-bury))))
(when (get-buffer "*test-sw-untouched-left*")
(kill-buffer "*test-sw-untouched-left*"))
diff --git a/tests/test-ai-vterm--terminal-guard.el b/tests/test-ai-vterm--terminal-guard.el
new file mode 100644
index 00000000..5a7971bf
--- /dev/null
+++ b/tests/test-ai-vterm--terminal-guard.el
@@ -0,0 +1,78 @@
+;;; test-ai-vterm--terminal-guard.el --- Tests for the terminal-frame guard -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; AI-vterm launches a graphical vterm side window, so it is GUI-only.
+;; `cj/--ai-vterm-refuse-in-terminal' signals a `user-error' when the
+;; current frame is a terminal frame; each interactive entry point
+;; (`cj/ai-vterm', `cj/ai-vterm-pick-project', `cj/ai-vterm-close')
+;; calls it first so F9 and friends decline -- with a message -- in a
+;; terminal frame instead of launching a vterm. The check is per-frame
+;; at command time, not at load, so a daemon serving both GUI and
+;; terminal frames keeps the launcher working in its GUI frames.
+;;
+;; `env-terminal-p' is mocked so the tests are deterministic regardless
+;; of whether the run itself is graphical (batch runs are terminal).
+
+;;; Code:
+
+(require 'ert)
+(require 'cl-lib)
+
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+(add-to-list 'load-path (expand-file-name "tests" user-emacs-directory))
+(require 'ai-vterm)
+
+;; ---------------------------- the guard helper ----------------------------
+
+(ert-deftest test-ai-vterm--refuse-in-terminal-errors-in-terminal-frame ()
+ "Error: terminal frame -> `user-error', so the command declines."
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () t)))
+ (should-error (cj/--ai-vterm-refuse-in-terminal) :type 'user-error)))
+
+(ert-deftest test-ai-vterm--refuse-in-terminal-passes-in-gui-frame ()
+ "Normal: GUI frame -> returns nil, no error, command proceeds."
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil)))
+ (should-not (cj/--ai-vterm-refuse-in-terminal))))
+
+;; ------------------- the three interactive entry points -------------------
+
+(ert-deftest test-ai-vterm-f9-declines-in-terminal-without-dispatching ()
+ "Error: F9 in a terminal frame errors and never reaches dispatch."
+ (let ((dispatched nil))
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () t))
+ ((symbol-function 'cj/--ai-vterm-dispatch)
+ (lambda () (setq dispatched t) '(pick-project))))
+ (should-error (cj/ai-vterm) :type 'user-error)
+ (should-not dispatched))))
+
+(ert-deftest test-ai-vterm-pick-project-declines-in-terminal-without-prompting ()
+ "Error: C-F9 in a terminal frame errors and never reaches the picker."
+ (let ((prompted nil))
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () t))
+ ((symbol-function 'cj/--ai-vterm-pick-project)
+ (lambda () (setq prompted t) "/tmp")))
+ (should-error (cj/ai-vterm-pick-project) :type 'user-error)
+ (should-not prompted))))
+
+(ert-deftest test-ai-vterm-close-declines-in-terminal-without-targeting ()
+ "Error: M-F9 in a terminal frame errors and never reaches close-target."
+ (let ((targeted nil))
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () t))
+ ((symbol-function 'cj/--ai-vterm-close-target)
+ (lambda () (setq targeted t) nil)))
+ (should-error (cj/ai-vterm-close) :type 'user-error)
+ (should-not targeted))))
+
+(ert-deftest test-ai-vterm-f9-passes-guard-in-gui-frame ()
+ "Normal: F9 in a GUI frame passes the guard and reaches dispatch."
+ (let ((dispatched nil))
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil))
+ ((symbol-function 'cj/--ai-vterm-dispatch)
+ (lambda () (setq dispatched t) '(pick-project)))
+ ((symbol-function 'cj/ai-vterm-pick-project)
+ (lambda (&optional _arg) nil)))
+ (cj/ai-vterm)
+ (should dispatched))))
+
+(provide 'test-ai-vterm--terminal-guard)
+;;; test-ai-vterm--terminal-guard.el ends here
diff --git a/tests/testutil-vterm-buffers.el b/tests/testutil-vterm-buffers.el
index 01a65d90..17f0a69a 100644
--- a/tests/testutil-vterm-buffers.el
+++ b/tests/testutil-vterm-buffers.el
@@ -9,6 +9,19 @@
;;; Code:
+(require 'cl-lib)
+
+(defun cj/test--call-as-gui (fn)
+ "Call FN with `env-terminal-p' stubbed to return nil (a GUI frame).
+
+The AI-vterm interactive commands refuse to run in a terminal frame
+via `cj/--ai-vterm-refuse-in-terminal'. A batch test run is itself a
+terminal frame, so tests that exercise the GUI-frame window behavior
+of those commands call them through this helper to present a GUI
+context."
+ (cl-letf (((symbol-function 'env-terminal-p) (lambda () nil)))
+ (funcall fn)))
+
(defun cj/test--kill-buffers-matching-prefix (prefix)
"Kill all live buffers whose name starts with PREFIX."
(dolist (b (buffer-list))
diff --git a/todo.org b/todo.org
index 6fa2161e..0b9f8c2e 100644
--- a/todo.org
+++ b/todo.org
@@ -153,6 +153,14 @@ What we're verifying: emoji glyphs + fonts apply in a GUI frame even when the fi
- in the GUI frame, open a buffer with an emoji and check it renders, and M-S-f / fonts look right
Expected: emoji renders and fonts are applied in the GUI frame.
+*** AI-vterm declines in a terminal frame, still launches in a GUI frame
+What we're verifying: the per-frame guard makes the F9 family decline — message only, no vterm — in a terminal frame, while a GUI frame still launches the agent.
+- emacsclient -t (TTY frame, off the running daemon)
+- in the TTY frame, press F9 (also try C-F9 and M-F9)
+- emacsclient -c (then a GUI frame)
+- in the GUI frame, press F9 and pick a project
+Expected: in the TTY frame the echo area shows "AI-vterm is GUI-only; not available in a terminal frame" and no vterm opens; in the GUI frame the project picker opens and the agent launches as before.
+
** DOING [#B] Consolidate to EAT as the single terminal :terminal:eval:
:PROPERTIES:
:LAST_REVIEWED: 2026-05-28