diff --git a/README.org b/README.org index b3a4f451..7ed7aa45 100644 --- a/README.org +++ b/README.org @@ -822,9 +822,11 @@ always go to Evil modes if you need to with ~C-z~). | agent-shell-qwen-acp-command | Command and parameters for the Qwen Code client. | | agent-shell-qwen-authentication | Configuration for Qwen Code authentication. | | agent-shell-qwen-environment | Environment variables for the Qwen Code client. | +| agent-shell-session-prompt-choices | Choices to show in the session selection prompt. | | agent-shell-session-restore-verbosity | How much prior context to show when restoring a session. | | agent-shell-screenshot-command | The program to use for capturing screenshots. | | agent-shell-section-functions | Abnormal hook run after overlays are applied (experimental). | +| agent-shell-session-prompt-exclude-choices | Kinds of choices to exclude from the session selection prompt. | | agent-shell-session-strategy | How to handle sessions when starting a new shell. | | agent-shell-show-busy-indicator | Non-nil to show the busy indicator animation in the header and mode line. | | agent-shell-show-config-icons | Whether to show icons in agent config selection. | diff --git a/agent-shell.el b/agent-shell.el index 6502399d..8a46e12b 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -692,6 +692,41 @@ Available values: (set-default sym value)) :group 'agent-shell) +(defcustom agent-shell-session-prompt-exclude-choices nil + "Kinds of choices to exclude from the session selection prompt. + +The session selection prompt offers a set of options when starting +a new shell: \"New shell\", \"New Downloads shell\", \"New +temp shell\", \"Switch to shell buffer\" (when other shells exist), +and one entry per existing ACP session. + +When nil (the default), all choices are shown. Otherwise, the +value is a list of keyword kinds; choices whose kind appears in +the list are excluded. Valid kinds are: + + `:new-shell' Start a new shell. + `:downloads-shell' Start a new downloads shell. + `:temp-shell' Start a new temporary shell. + `:other-shell' Switch to an existing shell buffer. + `:acp-session' Resume a previously saved ACP session. + +To always start a new shell without prompting, use +`agent-shell-session-strategy' with value `new' or `new-deferred' +instead. + +Example (hide the downloads and temp shell entries): + + (setq agent-shell-session-prompt-exclude-choices + \\='(:downloads-shell :temp-shell))" + :type '(choice (const :tag "Exclude nothing (show all)" nil) + (set :tag "Exclude selected kinds" + (const :tag "New shell" :new-shell) + (const :tag "New Downloads shell" :downloads-shell) + (const :tag "New temp shell" :temp-shell) + (const :tag "Switch to existing shell buffer" :other-shell) + (const :tag "Resume previous ACP session" :acp-session))) + :group 'agent-shell) + (defvar agent-shell-idle-timeout 30 "Seconds before an `idle' event is emitted. @@ -4976,6 +5011,72 @@ MAX-WIDTHS is an alist mapping column symbols to their max widths." (push (if face (propertize padded 'face face) padded) parts))) (apply #'concat (nreverse parts)))) +(defun agent-shell--filter-session-prompt-choices (choices) + "Filter CHOICES by `agent-shell-session-prompt-exclude-choices'. + +CHOICES is a list of alists, each shaped: + ((:kind . KIND) (:label . LABEL) (:value . VALUE)) + +When `agent-shell-session-prompt-exclude-choices' is nil, returns +CHOICES unchanged. Otherwise, drops any choice whose :kind is +listed in `agent-shell-session-prompt-exclude-choices'. + +Example: + (let ((agent-shell-session-prompt-exclude-choices \\='(:temp-shell))) + (agent-shell--filter-session-prompt-choices + \\='(((:kind . :new-shell) + (:label . \"New shell\") + (:value . nil)) + ((:kind . :temp-shell) + (:label . \"New temp shell\") + (:value . :temp-shell))))) + => (((:kind . :new-shell) + (:label . \"New shell\") + (:value . nil)))" + (if (null agent-shell-session-prompt-exclude-choices) + choices + (seq-remove (lambda (choice) + (memq (map-elt choice :kind) + agent-shell-session-prompt-exclude-choices)) + choices))) + +(cl-defun agent-shell--build-session-prompt-choices + (&key new-shell-label other-shell-p acp-sessions max-widths) + "Build the choices list shown by the session selection prompt. + +Always includes (in order) a `:new-shell' entry labeled +NEW-SHELL-LABEL, followed by `:downloads-shell' and `:temp-shell' +entries. + +When OTHER-SHELL-P is non-nil, an `:other-shell' entry is added. + +When ACP-SESSIONS is non-nil, one `:acp-session' entry is added +per session, with labels aligned using MAX-WIDTHS (see +`agent-shell--session-choice-label'). + +Each entry is an alist of the form + ((:kind . KIND) (:label . LABEL) (:value . VALUE))." + (append (list `((:kind . :new-shell) + (:label . ,new-shell-label) + (:value . nil))) + '(((:kind . :downloads-shell) + (:label . "New Downloads shell") + (:value . :downloads-shell)) + ((:kind . :temp-shell) + (:label . "New temp shell") + (:value . :temp-shell))) + (when other-shell-p + '(((:kind . :other-shell) + (:label . "Switch to shell buffer") + (:value . :other-shell)))) + (mapcar (lambda (acp-session) + `((:kind . :acp-session) + (:label . ,(agent-shell--session-choice-label + :acp-session acp-session + :max-widths max-widths)) + (:value . ,acp-session))) + acp-sessions))) + (defun agent-shell--prompt-select-session (acp-sessions) "Prompt to choose one from ACP-SESSIONS. @@ -4997,19 +5098,14 @@ Falls back to latest session in batch mode (e.g. tests)." (length (agent-shell--session-column-value col s))) acp-sessions)))) columns))) - ;; TODO: Consolidate choices with `agent-shell--shell-buffer'. - (session-choices (append (list (cons new-session-choice nil) - (cons "New Downloads shell" :downloads-shell) - (cons "New temp shell" :temp-shell)) - (when other-shells - (list (cons "Switch to shell buffer" :other-shell))) - (mapcar (lambda (acp-session) - (cons (agent-shell--session-choice-label - :acp-session acp-session - :max-widths max-widths) - acp-session)) - acp-sessions))) - (candidates (mapcar #'car session-choices)) + (session-choices + (agent-shell--filter-session-prompt-choices + (agent-shell--build-session-prompt-choices + :new-shell-label new-session-choice + :other-shell-p other-shells + :acp-sessions acp-sessions + :max-widths max-widths))) + (candidates (mapcar (lambda (c) (map-elt c :label)) session-choices)) ;; Some completion frameworks yielded appended (nil) to each line ;; unless this-command was bound. ;; @@ -5019,37 +5115,40 @@ Falls back to latest session in batch mode (e.g. tests)." ;; Let's optimize the rocket engine Feb 12, 21:02 (nil) (this-command 'agent-shell)) (agent-shell--emit-event :event 'session-prompt) - (let ((selection (completing-read "Start shell (default: new): " - (lambda (string pred action) - (if (eq action 'metadata) - '(metadata - (display-sort-function . identity) - (eager-display . t) - (eager-update . t)) - (complete-with-action action candidates string pred))) - nil t nil nil - new-session-choice))) - (pcase (map-elt session-choices selection) - (:other-shell - (let ((other-shell (agent-shell--read-shell-buffer - :prompt "Switch to shell buffer: " - :buffers other-shells)) - (bootstrapping-shell (map-elt (agent-shell--state) :buffer))) - (agent-shell--display-buffer other-shell) - (kill-buffer bootstrapping-shell) - :other-shell)) - (:downloads-shell - (let ((config (map-elt (agent-shell--state) :agent-config))) - (kill-buffer (map-elt (agent-shell--state) :buffer)) - (agent-shell-new-downloads-shell :config config)) - :other-shell) - (:temp-shell - (let ((config (map-elt (agent-shell--state) :agent-config))) - (kill-buffer (map-elt (agent-shell--state) :buffer)) - (agent-shell-new-temp-shell :config config)) - :other-shell) - (choice choice))))))) - + (when candidates + (let* ((selection (completing-read "Start shell (default: new): " + (lambda (string pred action) + (if (eq action 'metadata) + '(metadata + (display-sort-function . identity) + (eager-display . t) + (eager-update . t)) + (complete-with-action action candidates string pred))) + nil t nil nil + new-session-choice)) + (value (map-elt (seq-find (lambda (c) (equal (map-elt c :label) selection)) + session-choices) + :value))) + (pcase value + (:other-shell + (let ((other-shell (agent-shell--read-shell-buffer + :prompt "Switch to shell buffer: " + :buffers other-shells)) + (bootstrapping-shell (map-elt (agent-shell--state) :buffer))) + (agent-shell--display-buffer other-shell) + (kill-buffer bootstrapping-shell) + :other-shell)) + (:downloads-shell + (let ((config (map-elt (agent-shell--state) :agent-config))) + (kill-buffer (map-elt (agent-shell--state) :buffer)) + (agent-shell-new-downloads-shell :config config)) + :other-shell) + (:temp-shell + (let ((config (map-elt (agent-shell--state) :agent-config))) + (kill-buffer (map-elt (agent-shell--state) :buffer)) + (agent-shell-new-temp-shell :config config)) + :other-shell) + (choice choice)))))))) (cl-defun agent-shell--session-from-response (&key acp-response acp-session-id) "Return internal session state from ACP-RESPONSE and ACP-SESSION-ID." @@ -6041,6 +6140,7 @@ Returns a buffer object or nil." (user-error "No agent shell buffers available for current project")) (if (and (eq agent-shell-session-strategy 'new-deferred) (agent-shell-buffers)) +<<<<<<< HEAD ;; TODO: Consolidate choices with `agent-shell--prompt-select-session'. (let* ((start-new "New shell") (start-downloads "New Downloads shell") @@ -6062,6 +6162,36 @@ Returns a buffer object or nil." (error "No agent config found")) :no-focus t :new-session t)))) +======= + (let* ((choices (agent-shell--filter-session-prompt-choices + (agent-shell--build-session-prompt-choices + :new-shell-label "New shell" + :other-shell-p t))) + (selection (when choices + (completing-read "Start shell (default: new): " + (mapcar (lambda (c) (map-elt c :label)) choices) + nil t))) + (value (when selection + (map-elt (seq-find (lambda (c) (equal (map-elt c :label) selection)) + choices) + :value)))) + (pcase value + (:other-shell + (get-buffer (completing-read "Switch to shell buffer: " + (mapcar #'buffer-name (agent-shell-buffers)) + nil t))) + (:downloads-shell + (agent-shell-new-downloads-shell :no-display t)) + (:temp-shell + (agent-shell-new-temp-shell :no-display t)) + (_ + (agent-shell--start :config (or (agent-shell--resolve-preferred-config) + (agent-shell-select-config + :prompt "Start new agent: ") + (error "No agent config found")) + :no-focus t + :new-session t)))) +>>>>>>> 47b8a29 (Add agent-shell-session-prompt-choices defcustom) (agent-shell--start :config (or (agent-shell--resolve-preferred-config) (agent-shell-select-config :prompt "Start new agent: ") diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index 441f0eda..ae8e8df3 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -1890,6 +1890,95 @@ and rejects `new-deferred' and other unknown values." (should-error (agent-shell--validate-session-strategy 'bogus) :type 'user-error)) +(ert-deftest agent-shell--filter-session-prompt-choices-nil () + "Test that nil exclusion passes all choices through unchanged." + (let ((agent-shell-session-prompt-exclude-choices nil) + (choices '(((:kind . :new-shell) + (:label . "New shell") + (:value . nil)) + ((:kind . :downloads-shell) + (:label . "New Downloads shell") + (:value . :downloads-shell)) + ((:kind . :temp-shell) + (:label . "New temp shell") + (:value . :temp-shell)) + ((:kind . :other-shell) + (:label . "Switch to shell buffer") + (:value . :other-shell)) + ((:kind . :acp-session) + (:label . "Session A") + (:value . session-a))))) + (should (equal (agent-shell--filter-session-prompt-choices choices) + choices)))) + +(ert-deftest agent-shell--filter-session-prompt-choices-exclude () + "Test that an exclusion list drops choices with matching kind." + (let ((agent-shell-session-prompt-exclude-choices + '(:downloads-shell :temp-shell :other-shell))) + (should (equal (agent-shell--filter-session-prompt-choices + '(((:kind . :new-shell) + (:label . "New shell") + (:value . nil)) + ((:kind . :downloads-shell) + (:label . "New Downloads shell") + (:value . :downloads-shell)) + ((:kind . :temp-shell) + (:label . "New temp shell") + (:value . :temp-shell)) + ((:kind . :other-shell) + (:label . "Switch to shell buffer") + (:value . :other-shell)) + ((:kind . :acp-session) + (:label . "Session A") + (:value . session-a)))) + '(((:kind . :new-shell) + (:label . "New shell") + (:value . nil)) + ((:kind . :acp-session) + (:label . "Session A") + (:value . session-a))))))) + +(ert-deftest agent-shell--build-session-prompt-choices-minimal () + "Test the builder includes new/downloads/temp by default." + (should (equal (mapcar (lambda (c) (map-elt c :kind)) + (agent-shell--build-session-prompt-choices + :new-shell-label "Start new shell")) + '(:new-shell :downloads-shell :temp-shell))) + (should (equal (map-elt (car (agent-shell--build-session-prompt-choices + :new-shell-label "Start new shell")) + :label) + "Start new shell"))) + +(ert-deftest agent-shell--build-session-prompt-choices-other-shell () + "Test the builder appends :other-shell when OTHER-SHELL-P is non-nil." + (should-not (seq-find (lambda (c) (eq (map-elt c :kind) :other-shell)) + (agent-shell--build-session-prompt-choices + :new-shell-label "Start new shell"))) + (should (seq-find (lambda (c) (eq (map-elt c :kind) :other-shell)) + (agent-shell--build-session-prompt-choices + :new-shell-label "Start new shell" + :other-shell-p t)))) + +(ert-deftest agent-shell--build-session-prompt-choices-acp-sessions () + "Test the builder appends one :acp-session per session, preserving value." + (let* ((session-a '((sessionId . "s-1") + (title . "First") + (cwd . "/tmp") + (updatedAt . "2026-01-19T14:00:00Z"))) + (session-b '((sessionId . "s-2") + (title . "Second") + (cwd . "/tmp") + (updatedAt . "2026-01-20T16:00:00Z"))) + (choices (agent-shell--build-session-prompt-choices + :new-shell-label "Start new shell" + :acp-sessions (list session-a session-b))) + (session-choices (seq-filter (lambda (c) + (eq (map-elt c :kind) :acp-session)) + choices))) + (should (equal (length session-choices) 2)) + (should (equal (map-elt (nth 0 session-choices) :value) session-a)) + (should (equal (map-elt (nth 1 session-choices) :value) session-b)))) + (ert-deftest agent-shell--initiate-session-strategy-new-skips-list-load () "Test `agent-shell--initiate-session' skips list/load when strategy is `new'." (with-temp-buffer