Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down
218 changes: 174 additions & 44 deletions agent-shell.el
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand All @@ -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.
;;
Expand All @@ -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."
Expand Down Expand Up @@ -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")
Expand All @@ -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: ")
Expand Down
89 changes: 89 additions & 0 deletions tests/agent-shell-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading