From 4dc4f1a97cc27862d0627038efa18faabf7e3056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=84=A1=E5=90=8D=E6=B0=8F?= Date: Fri, 27 Mar 2026 14:10:41 +0800 Subject: [PATCH 1/3] Add a quit without confirm option --- pi-coding-agent-input.el | 4 +++- pi-coding-agent-ui.el | 8 ++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pi-coding-agent-input.el b/pi-coding-agent-input.el index 4c6e751..c9a1aa4 100644 --- a/pi-coding-agent-input.el +++ b/pi-coding-agent-input.el @@ -335,7 +335,8 @@ Only works when streaming is in progress." Kills both chat and input buffers, terminates the process, and removes the input window (merging its space with adjacent windows). -If a process is running, asks for confirmation first. If the user +If a process is running, asks for confirmation first unless +`pi-coding-agent-quit-without-confirmation' is non-nil. If the user cancels, the session remains intact." (interactive) (let* ((chat-buf (pi-coding-agent--get-chat-buffer)) @@ -346,6 +347,7 @@ cancels, the session remains intact." (input-windows nil)) (when (and proc-live (process-query-on-exit-flag proc) + (not pi-coding-agent-quit-without-confirmation) (not (yes-or-no-p "Pi session has a running process; quit anyway? "))) (user-error "Quit cancelled")) ;; Disable query flag to prevent double-ask on buffer kill diff --git a/pi-coding-agent-ui.el b/pi-coding-agent-ui.el index b6312c9..35b05f0 100644 --- a/pi-coding-agent-ui.el +++ b/pi-coding-agent-ui.el @@ -166,6 +166,14 @@ When nil (the default), only the visible text is copied." :type 'boolean :group 'pi-coding-agent) +(defcustom pi-coding-agent-quit-without-confirmation nil + "Whether `pi-coding-agent-quit' skips confirmation for a live process. +When non-nil, quitting a session never asks whether a running pi process +should be terminated. When nil, `pi-coding-agent-quit' prompts before +killing a live process that still has its query-on-exit flag enabled." + :type 'boolean + :group 'pi-coding-agent) + (defcustom pi-coding-agent-hot-tail-turn-count 3 "How many recent headed chat turns stay hot for redisplay refreshes. The hot tail is the suffix of the chat buffer beginning at the Nth newest From d2d4e02f6be26385d690c56924f7c78cb53fa90f Mon Sep 17 00:00:00 2001 From: Daniel Nouri Date: Fri, 27 Mar 2026 23:21:47 +0100 Subject: [PATCH 2/3] refactor: extract quit test session helper --- test/pi-coding-agent-menu-test.el | 133 +++++++++++++----------------- 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/test/pi-coding-agent-menu-test.el b/test/pi-coding-agent-menu-test.el index 6cbda99..46b1d1b 100644 --- a/test/pi-coding-agent-menu-test.el +++ b/test/pi-coding-agent-menu-test.el @@ -273,86 +273,69 @@ Also verifies that the new session-file is stored in state for reload to work." (should-not (get-buffer "*pi-coding-agent-chat:/tmp/pi-coding-agent-test-quit/*")) (should-not (get-buffer "*pi-coding-agent-input:/tmp/pi-coding-agent-test-quit/*")))) +(defmacro pi-coding-agent-test--with-quit-confirmable-session + (binding-spec &rest body) + "Run BODY with a pi session whose live process would prompt on quit. +BINDING-SPEC is (DIR CHAT-NAME INPUT-NAME PROC). DIR is evaluated once." + (declare (indent 1) (debug t)) + (let ((dir (nth 0 binding-spec)) + (chat-name (nth 1 binding-spec)) + (input-name (nth 2 binding-spec)) + (proc (nth 3 binding-spec)) + (dir-value (make-symbol "dir-value"))) + `(let* ((,dir-value ,dir) + (,chat-name (pi-coding-agent-test--chat-buffer-name ,dir-value)) + (,input-name (pi-coding-agent-test--input-buffer-name ,dir-value)) + (,proc nil)) + (make-directory ,dir-value t) + (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) + ((symbol-function 'pi-coding-agent--start-process) + (lambda (_) + (setq ,proc (start-process "pi-test-quit" nil "cat")) + (set-process-query-on-exit-flag ,proc t) + ,proc)) + ((symbol-function 'pi-coding-agent--display-buffers) #'ignore)) + (unwind-protect + (progn + (let ((default-directory ,dir-value)) + (pi-coding-agent)) + (with-current-buffer ,chat-name + (set-process-buffer ,proc (current-buffer))) + ,@body) + (when (and ,proc (process-live-p ,proc)) + (delete-process ,proc)) + (pi-coding-agent-test--kill-session-buffers ,dir-value)))))) + (ert-deftest pi-coding-agent-test-quit-cancelled-preserves-session () "When user cancels quit confirmation, both buffers remain intact and linked." - (let* ((dir "/tmp/pi-coding-agent-test-quit-cancel/") - (chat-name (concat "*pi-coding-agent-chat:" dir "*")) - (input-name (concat "*pi-coding-agent-input:" dir "*")) - (fake-proc nil)) - (make-directory dir t) - (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) - ((symbol-function 'pi-coding-agent--start-process) - (lambda (_) - ;; Create a real process that triggers kill confirmation - (setq fake-proc (start-process "pi-test-quit" nil "cat")) - (set-process-query-on-exit-flag fake-proc t) - fake-proc)) - ((symbol-function 'pi-coding-agent--display-buffers) #'ignore)) - (unwind-protect - (progn - (let ((default-directory dir)) - (pi-coding-agent)) - ;; Associate process with chat buffer (as the real code does) - (with-current-buffer chat-name - (set-process-buffer fake-proc (current-buffer))) - ;; User says "no" to quit confirmation - expect user-error - (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) nil))) - (with-current-buffer input-name - (should-error (pi-coding-agent-quit) :type 'user-error))) - ;; Both buffers should still exist - (should (get-buffer chat-name)) - (should (get-buffer input-name)) - ;; Buffers should still be linked - (with-current-buffer chat-name - (should (eq (pi-coding-agent--get-input-buffer) - (get-buffer input-name)))) - (with-current-buffer input-name - (should (eq (pi-coding-agent--get-chat-buffer) - (get-buffer chat-name))))) - ;; Cleanup - (when (and fake-proc (process-live-p fake-proc)) - (delete-process fake-proc)) - (ignore-errors (kill-buffer chat-name)) - (ignore-errors (kill-buffer input-name)))))) + (pi-coding-agent-test--with-quit-confirmable-session + ("/tmp/pi-coding-agent-test-quit-cancel/" chat-name input-name _proc) + (cl-letf (((symbol-function 'yes-or-no-p) (lambda (_) nil))) + (with-current-buffer input-name + (should-error (pi-coding-agent-quit) :type 'user-error))) + (should (get-buffer chat-name)) + (should (get-buffer input-name)) + (with-current-buffer chat-name + (should (eq (pi-coding-agent--get-input-buffer) + (get-buffer input-name)))) + (with-current-buffer input-name + (should (eq (pi-coding-agent--get-chat-buffer) + (get-buffer chat-name)))))) (ert-deftest pi-coding-agent-test-quit-confirmed-kills-both () "When user confirms quit, both buffers are killed without double-prompting." - (let* ((dir "/tmp/pi-coding-agent-test-quit-confirm/") - (chat-name (concat "*pi-coding-agent-chat:" dir "*")) - (input-name (concat "*pi-coding-agent-input:" dir "*")) - (fake-proc nil) - (prompt-count 0)) - (make-directory dir t) - (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) - ((symbol-function 'pi-coding-agent--start-process) - (lambda (_) - (setq fake-proc (start-process "pi-test-quit" nil "cat")) - (set-process-query-on-exit-flag fake-proc t) - fake-proc)) - ((symbol-function 'pi-coding-agent--display-buffers) #'ignore)) - (unwind-protect - (progn - (let ((default-directory dir)) - (pi-coding-agent)) - (with-current-buffer chat-name - (set-process-buffer fake-proc (current-buffer))) - ;; User says "yes" - count how many times we're asked - (cl-letf (((symbol-function 'yes-or-no-p) - (lambda (_) - (cl-incf prompt-count) - t))) - (with-current-buffer input-name - (pi-coding-agent-quit))) - ;; Both buffers should be gone - (should-not (get-buffer chat-name)) - (should-not (get-buffer input-name)) - ;; Should only be asked once (not twice due to cross-linked hooks) - (should (<= prompt-count 1))) - ;; Cleanup - (when (and fake-proc (process-live-p fake-proc)) - (delete-process fake-proc)) - (ignore-errors (kill-buffer chat-name)) - (ignore-errors (kill-buffer input-name)))))) + (let ((prompt-count 0)) + (pi-coding-agent-test--with-quit-confirmable-session + ("/tmp/pi-coding-agent-test-quit-confirm/" chat-name input-name _proc) + (cl-letf (((symbol-function 'yes-or-no-p) + (lambda (_) + (cl-incf prompt-count) + t))) + (with-current-buffer input-name + (pi-coding-agent-quit))) + (should-not (get-buffer chat-name)) + (should-not (get-buffer input-name)) + (should (<= prompt-count 1))))) (ert-deftest pi-coding-agent-test-kill-chat-kills-input () "Killing chat buffer also kills input buffer." From 5fdd82ded059b90eb391dbe84155b758cdefe767 Mon Sep 17 00:00:00 2001 From: Daniel Nouri Date: Fri, 27 Mar 2026 23:22:14 +0100 Subject: [PATCH 3/3] test: cover quit without confirmation --- test/pi-coding-agent-menu-test.el | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/pi-coding-agent-menu-test.el b/test/pi-coding-agent-menu-test.el index 46b1d1b..857dab5 100644 --- a/test/pi-coding-agent-menu-test.el +++ b/test/pi-coding-agent-menu-test.el @@ -337,6 +337,19 @@ BINDING-SPEC is (DIR CHAT-NAME INPUT-NAME PROC). DIR is evaluated once." (should-not (get-buffer input-name)) (should (<= prompt-count 1))))) +(ert-deftest pi-coding-agent-test-quit-without-confirmation-kills-both-without-prompt () + "When configured, quitting a live session kills both buffers without prompting." + (let ((pi-coding-agent-quit-without-confirmation t)) + (pi-coding-agent-test--with-quit-confirmable-session + ("/tmp/pi-coding-agent-test-quit-no-confirm/" chat-name input-name _proc) + (cl-letf (((symbol-function 'yes-or-no-p) + (lambda (&rest _) + (ert-fail "pi-coding-agent-quit prompted unexpectedly")))) + (with-current-buffer input-name + (pi-coding-agent-quit))) + (should-not (get-buffer chat-name)) + (should-not (get-buffer input-name))))) + (ert-deftest pi-coding-agent-test-kill-chat-kills-input () "Killing chat buffer also kills input buffer." (pi-coding-agent-test-with-mock-session "/tmp/pi-coding-agent-test-linked/"