From 091778624b00a678c858ba4ea9859471079595b8 Mon Sep 17 00:00:00 2001 From: John Buckley Date: Wed, 29 Jul 2026 13:55:50 +0100 Subject: [PATCH 1/5] Add default-thought-level-id to agent-shell-make-agent-config --- agent-shell.el | 68 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/agent-shell.el b/agent-shell.el index 67d2a8b3..fe931dad 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -710,6 +710,7 @@ Each element can be: authenticate-request-maker default-model-id default-session-mode-id + default-thought-level-id session-meta mcp-servers notification-adapter @@ -729,6 +730,9 @@ Keyword arguments: - AUTHENTICATE-REQUEST-MAKER: Function to create authentication requests - DEFAULT-MODEL-ID: Default model ID (function returning value). - DEFAULT-SESSION-MODE-ID: Default session mode ID (function returning value). +- DEFAULT-THOUGHT-LEVEL-ID: Default thought level ID (function returning + value). Applied after the default model, since the levels an agent + offers typically depend on the active model. - SESSION-META: Optional alist of agent-specific metadata sent as `_meta' with session-creating requests (`session/new', `session/load', `session/resume', and `session/fork'). @@ -751,6 +755,7 @@ Returns an alist with all specified values." (:authenticate-request-maker . ,authenticate-request-maker) ;; function (:default-model-id . ,default-model-id) ;; function (:default-session-mode-id . ,default-session-mode-id) ;; function + (:default-thought-level-id . ,default-thought-level-id) ;; function (:session-meta . ,session-meta) (:mcp-servers . ,mcp-servers) (:notification-adapter . ,notification-adapter) ;; function @@ -1189,6 +1194,7 @@ OUTGOING-REQUEST-DECORATOR (passed through to `acp-make-client')." (cons :authenticated nil) (cons :set-model nil) (cons :set-session-mode nil) + (cons :set-thought-level nil) (cons :session (list (cons :id nil) (cons :config-options nil) (cons :model-id nil) @@ -2252,6 +2258,18 @@ Flow: :on-mode-changed (lambda () (map-put! (agent-shell--state) :set-session-mode t) (agent-shell--handle :command command :shell-buffer shell-buffer)))) + ;; Send ACP request to set default thought level (optional) + ;; Comes after the default model, since available thought levels + ;; are typically model-specific. + ((and (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id)) + (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id))) + (not (map-elt (agent-shell--state) :set-thought-level))) + (agent-shell--set-default-thought-level + :shell-buffer shell-buffer + :thought-level-id (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id))) + :on-thought-level-changed (lambda () + (map-put! (agent-shell--state) :set-thought-level t) + (agent-shell--handle :command command :shell-buffer shell-buffer)))) ;; Initialization complete (t (agent-shell--emit-event :event 'init-finished) @@ -4046,7 +4064,8 @@ For example, shut down ACP client." (map-put! (agent-shell--state) :initialized nil) (map-put! (agent-shell--state) :authenticated nil) (map-put! (agent-shell--state) :set-model nil) - (map-put! (agent-shell--state) :set-session-mode nil)) + (map-put! (agent-shell--state) :set-session-mode nil) + (map-put! (agent-shell--state) :set-thought-level nil)) (agent-shell-heartbeat-stop :heartbeat (map-elt (agent-shell--state) :heartbeat))) @@ -6070,6 +6089,7 @@ Initialization events (emitted in order): `init-session' - ACP session created `init-model' - Default model set (optional) `init-session-mode' - Default session mode set (optional) + `init-thought-level' - Default thought level set (optional) `session-list' - Session list fetch initiated `session-prompt' - About to prompt user for session selection `session-selected' - Session chosen (new or existing) @@ -6615,6 +6635,41 @@ Call ON-MODE-CHANGED on success." :on-failure (agent-shell--make-error-handler :state (agent-shell--state) :shell-buffer shell-buffer)))) +(cl-defun agent-shell--set-default-thought-level (&key shell-buffer thought-level-id on-thought-level-changed) + "Set default thought level to THOUGHT-LEVEL-ID in SHELL-BUFFER. +Call ON-THOUGHT-LEVEL-CHANGED on success. + +Agents only advertise thought levels for models supporting them. When +unavailable, report it and carry on with initialization." + (when (map-nested-elt (agent-shell--state) '(:session :id)) + (with-current-buffer (map-elt agent-shell--state :buffer) + (agent-shell--update-bootstrapping-fragment + :state (agent-shell--state) + :block-id "set-thought-level" + :label-left (propertize "Setting thought level" 'font-lock-face 'agent-shell-section-heading) + :body (format "Requesting %s..." thought-level-id))) + (if (agent-shell--config-option-by-category (agent-shell--state) "thought_level") + (agent-shell--config-option-set-thought-level-id + :thought-level-id thought-level-id + :on-success (lambda () + (agent-shell--update-bootstrapping-fragment + :state (agent-shell--state) + :block-id "set-thought-level" + :body "\n\nDone" + :append t) + (agent-shell--emit-event :event 'init-thought-level) + (when on-thought-level-changed + (funcall on-thought-level-changed))) + :on-failure (agent-shell--make-error-handler + :state (agent-shell--state) :shell-buffer shell-buffer)) + (agent-shell--update-bootstrapping-fragment + :state (agent-shell--state) + :block-id "set-thought-level" + :body "\n\nNot available for this session" + :append t) + (when on-thought-level-changed + (funcall on-thought-level-changed))))) + (cl-defun agent-shell--initiate-session (&key shell-buffer on-session-init) "Initiate ACP session creation with SHELL-BUFFER. @@ -6937,7 +6992,16 @@ overwrites an existing fragment with equivalent content." :block-id "set-session-mode" :label-left (propertize "Setting session mode" 'font-lock-face 'agent-shell-section-heading) - :body (format "Requesting %s..." mode-id)))) + :body (format "Requesting %s..." mode-id))) + (when-let* ((id-fn (map-nested-elt state '(:agent-config :default-thought-level-id))) + (thought-level-id (funcall id-fn)) + ((not (map-elt state :set-thought-level)))) + (agent-shell--update-bootstrapping-fragment + :state state + :block-id "set-thought-level" + :label-left (propertize "Setting thought level" + 'font-lock-face 'agent-shell-section-heading) + :body (format "Requesting %s..." thought-level-id)))) (defun agent-shell--display-session-options () "Display available session options during bootstrapping." From 7e4f9128ed7d4cd9f1a9064bbe9693aed4c631e8 Mon Sep 17 00:00:00 2001 From: John Buckley Date: Wed, 29 Jul 2026 13:56:16 +0100 Subject: [PATCH 2/5] Add agent-shell-opencode-default-model-variant defcustom --- README.org | 1 + agent-shell-opencode.el | 15 ++++++++++ tests/agent-shell-opencode-tests.el | 43 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 tests/agent-shell-opencode-tests.el diff --git a/README.org b/README.org index 3394a4fe..7fd93c6f 100644 --- a/README.org +++ b/README.org @@ -1051,6 +1051,7 @@ always go to Evil modes if you need to with ~C-z~). | agent-shell-opencode-acp-command | Command and parameters for the OpenCode client. | | agent-shell-opencode-authentication | Configuration for OpenCode authentication. | | agent-shell-opencode-default-model-id | Default OpenCode model ID. | +| agent-shell-opencode-default-model-variant | Default OpenCode model variant. | | agent-shell-opencode-default-session-mode-id | Default OpenCode session mode ID. | | agent-shell-opencode-environment | Environment variables for the OpenCode client. | | agent-shell-path-resolver-function | Function for resolving remote paths on the local file-system, and vice versa. | diff --git a/agent-shell-opencode.el b/agent-shell-opencode.el index 70ef76b6..64034768 100644 --- a/agent-shell-opencode.el +++ b/agent-shell-opencode.el @@ -80,6 +80,20 @@ when starting a new shell." :type '(choice (const nil) string) :group 'agent-shell) +(defcustom agent-shell-opencode-default-model-variant + nil + "Default OpenCode model variant. + +OpenCode exposes model variants (provider-specific reasoning effort) as +the \"Effort\" option displayed under \"Available config options\" when +starting a new shell, for example \"low\", \"high\" or \"max\". + +Variants are model-specific, so this is applied after +`agent-shell-opencode-default-model-id'. It is ignored for models +offering no variants." + :type '(choice (const nil) string) + :group 'agent-shell) + (defcustom agent-shell-opencode-default-session-mode-id nil "Default OpenCode session mode ID. @@ -129,6 +143,7 @@ Returns an agent configuration alist using `agent-shell-make-agent-config'." (agent-shell-opencode-make-client :buffer buffer)) :default-model-id (lambda () agent-shell-opencode-default-model-id) :default-session-mode-id (lambda () agent-shell-opencode-default-session-mode-id) + :default-thought-level-id (lambda () agent-shell-opencode-default-model-variant) :install-instructions "See https://opencode.ai/docs for installation.")) ;;;###autoload diff --git a/tests/agent-shell-opencode-tests.el b/tests/agent-shell-opencode-tests.el new file mode 100644 index 00000000..d5e7f350 --- /dev/null +++ b/tests/agent-shell-opencode-tests.el @@ -0,0 +1,43 @@ +;;; agent-shell-opencode-tests.el --- Tests for agent-shell-opencode -*- lexical-binding: t; -*- + +(require 'ert) +(require 'agent-shell) +(require 'agent-shell-opencode) + +;;; Code: + +(ert-deftest agent-shell-opencode-default-model-id-test () + "Test that OpenCode config exposes default model id." + (let ((default-model-id-fn + (map-elt (agent-shell-opencode-make-agent-config) :default-model-id))) + + (let ((agent-shell-opencode-default-model-id nil)) + (should (null (funcall default-model-id-fn)))) + + (let ((agent-shell-opencode-default-model-id "anthropic/claude-opus-4-5")) + (should (string= (funcall default-model-id-fn) "anthropic/claude-opus-4-5"))))) + +(ert-deftest agent-shell-opencode-default-model-variant-test () + "Test that OpenCode config exposes default model variant as thought level." + (let ((default-thought-level-id-fn + (map-elt (agent-shell-opencode-make-agent-config) :default-thought-level-id))) + + (let ((agent-shell-opencode-default-model-variant nil)) + (should (null (funcall default-thought-level-id-fn)))) + + (let ((agent-shell-opencode-default-model-variant "high")) + (should (string= (funcall default-thought-level-id-fn) "high"))))) + +(ert-deftest agent-shell-opencode-default-session-mode-id-test () + "Test that OpenCode config exposes default session mode id." + (let ((default-session-mode-id-fn + (map-elt (agent-shell-opencode-make-agent-config) :default-session-mode-id))) + + (let ((agent-shell-opencode-default-session-mode-id nil)) + (should (null (funcall default-session-mode-id-fn)))) + + (let ((agent-shell-opencode-default-session-mode-id "plan")) + (should (string= (funcall default-session-mode-id-fn) "plan"))))) + +(provide 'agent-shell-opencode-tests) +;;; agent-shell-opencode-tests.el ends here From d7dd545d221c0da12f5cadfdc0717f2e91c3e2a3 Mon Sep 17 00:00:00 2001 From: John Buckley Date: Tue, 25 Aug 2026 13:27:02 +0100 Subject: [PATCH 3/5] Add default-config-options to agent-shell-make-agent-config --- agent-shell-config.el | 58 +++++++++++++++++ agent-shell.el | 129 ++++++++++++++++++++++++------------- tests/agent-shell-tests.el | 111 +++++++++++++++++++++++++++++++ 3 files changed, 252 insertions(+), 46 deletions(-) diff --git a/agent-shell-config.el b/agent-shell-config.el index 8a3c0cba..5620b89e 100644 --- a/agent-shell-config.el +++ b/agent-shell-config.el @@ -144,6 +144,64 @@ For example: matches) (car matches)))) +(defun agent-shell--resolve-config-option (state option) + "Return the config option in STATE addressed by OPTION, or nil. + +OPTION is matched against advertised ids first, then ACP categories, so +both what a shell lists under \"Available config options\" (\"effort\") +and the spec's category names (\"thought_level\") reach the same option. +Ids cast the wider net: an option outside the spec's categories, say +\"fast\", is only addressable by id. + +For example, against an agent advertising an \"effort\" option +categorized as \"thought_level\": + + (agent-shell--resolve-config-option state \"effort\") + => \\='((:id . \"effort\") (:category . \"thought_level\") ...) + + (agent-shell--resolve-config-option state \"thought_level\") + => \\='((:id . \"effort\") (:category . \"thought_level\") ...)" + (or (agent-shell--config-option-get :state state :id option) + (agent-shell--config-option-by-category state option))) + +(defun agent-shell--config-option-offers-value-p (option value) + "Return non-nil when OPTION offers VALUE. + +Only options enumerating their values can be checked, so an option +advertising none (a free-form string option, for example) accepts any +VALUE. + +For example: + + (agent-shell--config-option-offers-value-p + \\='((:options . (((:value . \"low\")) ((:value . \"high\"))))) \"high\") + => t" + (or (seq-empty-p (map-elt option :options)) + (seq-find (lambda (candidate) + (equal value (map-elt candidate :value))) + (map-elt option :options)))) + +(defun agent-shell--config-option-skip-reason (state option) + "Explain why OPTION could not be set in STATE. + +Names the ids the agent does offer, since agents advertise options +conditionally and scope their values to the active model. + +For example: + + (agent-shell--config-option-skip-reason state \"effort\") + => \"agent offers low, high, max\" + + (agent-shell--config-option-skip-reason state \"fast\") + => \"agent advertises no fast option\"" + (if-let* ((resolved (agent-shell--resolve-config-option state option))) + (format "agent offers %s" + (string-join (seq-map (lambda (candidate) + (map-elt candidate :value)) + (map-elt resolved :options)) + ", ")) + (format "agent advertises no %s option" option))) + (defun agent-shell--select-config-options (state) "Return selectable (type = \"select\") config options from STATE." (seq-filter (lambda (option) diff --git a/agent-shell.el b/agent-shell.el index fe931dad..01a8d801 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -710,7 +710,7 @@ Each element can be: authenticate-request-maker default-model-id default-session-mode-id - default-thought-level-id + default-config-options session-meta mcp-servers notification-adapter @@ -730,9 +730,13 @@ Keyword arguments: - AUTHENTICATE-REQUEST-MAKER: Function to create authentication requests - DEFAULT-MODEL-ID: Default model ID (function returning value). - DEFAULT-SESSION-MODE-ID: Default session mode ID (function returning value). -- DEFAULT-THOUGHT-LEVEL-ID: Default thought level ID (function returning - value). Applied after the default model, since the levels an agent - offers typically depend on the active model. +- DEFAULT-CONFIG-OPTIONS: Default ACP session config options (function + returning an alist of (OPTION . VALUE), both strings). OPTION is + matched against the ids the agent advertises, falling back to ACP + categories (\"model\", \"mode\", \"thought_level\"). Applied in the + order listed, after DEFAULT-MODEL-ID and DEFAULT-SESSION-MODE-ID. + Order matters: options an agent scopes to the active model (thought + level, for example) must follow the option selecting that model. - SESSION-META: Optional alist of agent-specific metadata sent as `_meta' with session-creating requests (`session/new', `session/load', `session/resume', and `session/fork'). @@ -755,7 +759,7 @@ Returns an alist with all specified values." (:authenticate-request-maker . ,authenticate-request-maker) ;; function (:default-model-id . ,default-model-id) ;; function (:default-session-mode-id . ,default-session-mode-id) ;; function - (:default-thought-level-id . ,default-thought-level-id) ;; function + (:default-config-options . ,default-config-options) ;; function (:session-meta . ,session-meta) (:mcp-servers . ,mcp-servers) (:notification-adapter . ,notification-adapter) ;; function @@ -1194,7 +1198,7 @@ OUTGOING-REQUEST-DECORATOR (passed through to `acp-make-client')." (cons :authenticated nil) (cons :set-model nil) (cons :set-session-mode nil) - (cons :set-thought-level nil) + (cons :set-config-options nil) (cons :session (list (cons :id nil) (cons :config-options nil) (cons :model-id nil) @@ -2258,18 +2262,16 @@ Flow: :on-mode-changed (lambda () (map-put! (agent-shell--state) :set-session-mode t) (agent-shell--handle :command command :shell-buffer shell-buffer)))) - ;; Send ACP request to set default thought level (optional) - ;; Comes after the default model, since available thought levels - ;; are typically model-specific. - ((and (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id)) - (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id))) - (not (map-elt (agent-shell--state) :set-thought-level))) - (agent-shell--set-default-thought-level + ;; Send ACP requests to set default config options (optional) + ((and (map-nested-elt (agent-shell--state) '(:agent-config :default-config-options)) + (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-config-options))) + (not (map-elt (agent-shell--state) :set-config-options))) + (agent-shell--set-default-config-options :shell-buffer shell-buffer - :thought-level-id (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-thought-level-id))) - :on-thought-level-changed (lambda () - (map-put! (agent-shell--state) :set-thought-level t) - (agent-shell--handle :command command :shell-buffer shell-buffer)))) + :config-options (funcall (map-nested-elt (agent-shell--state) '(:agent-config :default-config-options))) + :on-options-set (lambda () + (map-put! (agent-shell--state) :set-config-options t) + (agent-shell--handle :command command :shell-buffer shell-buffer)))) ;; Initialization complete (t (agent-shell--emit-event :event 'init-finished) @@ -4065,7 +4067,7 @@ For example, shut down ACP client." (map-put! (agent-shell--state) :authenticated nil) (map-put! (agent-shell--state) :set-model nil) (map-put! (agent-shell--state) :set-session-mode nil) - (map-put! (agent-shell--state) :set-thought-level nil)) + (map-put! (agent-shell--state) :set-config-options nil)) (agent-shell-heartbeat-stop :heartbeat (map-elt (agent-shell--state) :heartbeat))) @@ -6089,7 +6091,7 @@ Initialization events (emitted in order): `init-session' - ACP session created `init-model' - Default model set (optional) `init-session-mode' - Default session mode set (optional) - `init-thought-level' - Default thought level set (optional) + `init-config-options' - Default config options applied (optional) `session-list' - Session list fetch initiated `session-prompt' - About to prompt user for session selection `session-selected' - Session chosen (new or existing) @@ -6635,40 +6637,76 @@ Call ON-MODE-CHANGED on success." :on-failure (agent-shell--make-error-handler :state (agent-shell--state) :shell-buffer shell-buffer)))) -(cl-defun agent-shell--set-default-thought-level (&key shell-buffer thought-level-id on-thought-level-changed) - "Set default thought level to THOUGHT-LEVEL-ID in SHELL-BUFFER. -Call ON-THOUGHT-LEVEL-CHANGED on success. +(cl-defun agent-shell--set-default-config-options (&key shell-buffer config-options (first t) on-options-set) + "Apply CONFIG-OPTIONS in SHELL-BUFFER, one at a time, in order. -Agents only advertise thought levels for models supporting them. When -unavailable, report it and carry on with initialization." +CONFIG-OPTIONS is an alist of (OPTION . VALUE), as described in +`agent-shell-make-agent-config'. Applying them in sequence (rather +than concurrently) lets an earlier entry determine what a later one can +choose from, since agents re-advertise their options on every change. + +FIRST tracks whether the next entry opens the progress report, and is +managed by the recursion. + +Call ON-OPTIONS-SET once the list is exhausted." + (if-let* ((entry (car config-options))) + (agent-shell--set-default-config-option + :shell-buffer shell-buffer + :option (car entry) + :value (cdr entry) + :first first + :on-option-set (lambda () + (agent-shell--set-default-config-options + :shell-buffer shell-buffer + :config-options (cdr config-options) + :first nil + :on-options-set on-options-set))) + (agent-shell--emit-event :event 'init-config-options) + (when on-options-set + (funcall on-options-set)))) + +(cl-defun agent-shell--set-default-config-option (&key shell-buffer option value first on-option-set) + "Set config OPTION to VALUE in SHELL-BUFFER, then call ON-OPTION-SET. + +OPTION is matched against advertised option ids first, then ACP +categories. Agents advertise options conditionally (thought levels +only for models supporting them, for example) and scope values to the +active model, so an unknown option or value is reported and skipped +rather than aborting initialization. + +FIRST reports this as the opening line of the shared progress block, +which later entries append their own line to." (when (map-nested-elt (agent-shell--state) '(:session :id)) (with-current-buffer (map-elt agent-shell--state :buffer) (agent-shell--update-bootstrapping-fragment :state (agent-shell--state) - :block-id "set-thought-level" - :label-left (propertize "Setting thought level" 'font-lock-face 'agent-shell-section-heading) - :body (format "Requesting %s..." thought-level-id))) - (if (agent-shell--config-option-by-category (agent-shell--state) "thought_level") - (agent-shell--config-option-set-thought-level-id - :thought-level-id thought-level-id + :block-id "set-config-options" + :label-left (propertize "Setting config options" 'font-lock-face 'agent-shell-section-heading) + :body (format "%s%s: requesting %s..." (if first "" "\n") option value) + :append t)) + (if-let* ((resolved (agent-shell--resolve-config-option (agent-shell--state) option)) + ((agent-shell--config-option-offers-value-p resolved value))) + (agent-shell--set-session-config-option + :config-id (map-elt resolved :id) + :value value :on-success (lambda () (agent-shell--update-bootstrapping-fragment :state (agent-shell--state) - :block-id "set-thought-level" - :body "\n\nDone" + :block-id "set-config-options" + :body " done" :append t) - (agent-shell--emit-event :event 'init-thought-level) - (when on-thought-level-changed - (funcall on-thought-level-changed))) + (when on-option-set + (funcall on-option-set))) :on-failure (agent-shell--make-error-handler :state (agent-shell--state) :shell-buffer shell-buffer)) (agent-shell--update-bootstrapping-fragment :state (agent-shell--state) - :block-id "set-thought-level" - :body "\n\nNot available for this session" + :block-id "set-config-options" + :body (format " skipped (%s)" + (agent-shell--config-option-skip-reason (agent-shell--state) option)) :append t) - (when on-thought-level-changed - (funcall on-thought-level-changed))))) + (when on-option-set + (funcall on-option-set))))) (cl-defun agent-shell--initiate-session (&key shell-buffer on-session-init) "Initiate ACP session creation with SHELL-BUFFER. @@ -6993,15 +7031,14 @@ overwrites an existing fragment with equivalent content." :label-left (propertize "Setting session mode" 'font-lock-face 'agent-shell-section-heading) :body (format "Requesting %s..." mode-id))) - (when-let* ((id-fn (map-nested-elt state '(:agent-config :default-thought-level-id))) - (thought-level-id (funcall id-fn)) - ((not (map-elt state :set-thought-level)))) + (when-let* ((options-fn (map-nested-elt state '(:agent-config :default-config-options))) + ((funcall options-fn)) + ((not (map-elt state :set-config-options)))) (agent-shell--update-bootstrapping-fragment :state state - :block-id "set-thought-level" - :label-left (propertize "Setting thought level" - 'font-lock-face 'agent-shell-section-heading) - :body (format "Requesting %s..." thought-level-id)))) + :block-id "set-config-options" + :label-left (propertize "Setting config options" + 'font-lock-face 'agent-shell-section-heading)))) (defun agent-shell--display-session-options () "Display available session options during bootstrapping." diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index c20321b9..ae319664 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -1514,6 +1514,117 @@ the category, the option is still returned." (should (equal (map-elt (agent-shell--config-option-by-category state "model") :id) "model_id")))) +(defun agent-shell-tests--opencode-config-options-state () + "Return state advertising the config options OpenCode reports over ACP. + +Its thought level option is `effort', categorized \"thought_level\", and +its \"fast\" option carries no category at all." + (list (cons :config-options + (agent-shell--normalize-config-options + [((id . "model") + (name . "Model") + (category . "model") + (type . "select") + (currentValue . "anthropic/claude-opus-4-5") + (options . [((value . "anthropic/claude-opus-4-5") + (name . "Claude Opus 4.5"))])) + ((id . "effort") + (name . "Effort") + (category . "thought_level") + (type . "select") + (currentValue . "low") + (options . [((value . "low") (name . "Low")) + ((value . "high") (name . "High")) + ((value . "max") (name . "Max"))])) + ((id . "fast") + (name . "Fast mode") + (type . "select") + (currentValue . "off") + (options . [((value . "on") (name . "On")) + ((value . "off") (name . "Off"))]))])))) + +(ert-deftest agent-shell--resolve-config-option-by-id-test () + "Test `agent-shell--resolve-config-option' matches advertised ids. + +Ids are what a shell lists under \"Available config options\", and are +the only way to reach an option carrying no ACP category." + (let ((state (agent-shell-tests--opencode-config-options-state))) + (should (equal (map-elt (agent-shell--resolve-config-option state "effort") :id) + "effort")) + (should (equal (map-elt (agent-shell--resolve-config-option state "fast") :id) + "fast")) + (should-not (agent-shell--resolve-config-option state "nonexistent")))) + +(ert-deftest agent-shell--resolve-config-option-by-category-test () + "Test `agent-shell--resolve-config-option' falls back to ACP categories. + +OpenCode names its thought level option `effort', so the spec's +\"thought_level\" category has to resolve to it." + (let ((state (agent-shell-tests--opencode-config-options-state))) + (should (equal (map-elt (agent-shell--resolve-config-option state "thought_level") :id) + "effort")) + (should (equal (map-elt (agent-shell--resolve-config-option state "model") :id) + "model")))) + +(ert-deftest agent-shell--config-option-offers-value-test () + "Test `agent-shell--config-option-offers-value-p'." + (let ((effort (agent-shell--resolve-config-option + (agent-shell-tests--opencode-config-options-state) "effort"))) + (should (agent-shell--config-option-offers-value-p effort "high")) + (should-not (agent-shell--config-option-offers-value-p effort "turbo"))) + ;; An option enumerating no values accepts anything. + (should (agent-shell--config-option-offers-value-p '((:options . nil)) "anything"))) + +(ert-deftest agent-shell--config-option-skip-reason-test () + "Test `agent-shell--config-option-skip-reason'." + (let ((state (agent-shell-tests--opencode-config-options-state))) + (should (equal (agent-shell--config-option-skip-reason state "effort") + "agent offers low, high, max")) + (should (equal (agent-shell--config-option-skip-reason state "reasoning") + "agent advertises no reasoning option")))) + +(ert-deftest agent-shell--set-default-config-options-test () + "Test `agent-shell--set-default-config-options' applies options in order. + +Entries resolve by id or category, are sent one at a time in the order +listed, and anything the agent does not offer is skipped without +stalling the rest of initialization." + (let ((sent nil) + (finished nil)) + (with-temp-buffer + (setq-local agent-shell--state + (append (list (cons :buffer (current-buffer)) + (cons :session (list (cons :id "session-1")))) + (agent-shell-tests--opencode-config-options-state))) + (cl-letf (((symbol-function 'agent-shell--state) + (lambda () agent-shell--state)) + ((symbol-function 'agent-shell--update-bootstrapping-fragment) + (lambda (&rest _))) + ((symbol-function 'agent-shell--emit-event) + (lambda (&rest _))) + ((symbol-function 'agent-shell--set-session-config-option) + (lambda (&rest args) + (push (cons (plist-get args :config-id) + (plist-get args :value)) + sent) + (funcall (plist-get args :on-success))))) + (agent-shell--set-default-config-options + :shell-buffer (current-buffer) + :config-options '(("model" . "anthropic/claude-opus-4-5") + ("thought_level" . "high") + ("fast" . "on") + ("reasoning" . "high") + ("effort" . "turbo")) + :on-options-set (lambda () (setq finished t))))) + ;; "thought_level" resolves to the "effort" id OpenCode advertises, + ;; while the unadvertised "reasoning" option and the "turbo" value + ;; "effort" does not offer are both skipped. + (should (equal (reverse sent) + '(("model" . "anthropic/claude-opus-4-5") + ("effort" . "high") + ("fast" . "on")))) + (should finished))) + (ert-deftest agent-shell--session-from-response-config-options-test () "Test `agent-shell--session-from-response' stores config options." (let ((session (agent-shell--session-from-response From f6e0a039131755090f6c53038abe963b84265f71 Mon Sep 17 00:00:00 2001 From: John Buckley Date: Tue, 25 Aug 2026 13:27:34 +0100 Subject: [PATCH 4/5] Replace agent-shell-opencode-default-model-variant with agent-shell-opencode-default-config-options --- README.org | 2 +- agent-shell-opencode.el | 35 +++++++++++++++++++---------- tests/agent-shell-opencode-tests.el | 24 +++++++++++--------- 3 files changed, 38 insertions(+), 23 deletions(-) diff --git a/README.org b/README.org index 7fd93c6f..cc417434 100644 --- a/README.org +++ b/README.org @@ -1050,8 +1050,8 @@ always go to Evil modes if you need to with ~C-z~). | agent-shell-openai-default-session-mode-id | Default Codex session mode ID. | | agent-shell-opencode-acp-command | Command and parameters for the OpenCode client. | | agent-shell-opencode-authentication | Configuration for OpenCode authentication. | +| agent-shell-opencode-default-config-options | Default OpenCode config options, applied at session start. | | agent-shell-opencode-default-model-id | Default OpenCode model ID. | -| agent-shell-opencode-default-model-variant | Default OpenCode model variant. | | agent-shell-opencode-default-session-mode-id | Default OpenCode session mode ID. | | agent-shell-opencode-environment | Environment variables for the OpenCode client. | | agent-shell-path-resolver-function | Function for resolving remote paths on the local file-system, and vice versa. | diff --git a/agent-shell-opencode.el b/agent-shell-opencode.el index 64034768..9d5c3783 100644 --- a/agent-shell-opencode.el +++ b/agent-shell-opencode.el @@ -80,18 +80,29 @@ when starting a new shell." :type '(choice (const nil) string) :group 'agent-shell) -(defcustom agent-shell-opencode-default-model-variant +(defcustom agent-shell-opencode-default-config-options nil - "Default OpenCode model variant. - -OpenCode exposes model variants (provider-specific reasoning effort) as -the \"Effort\" option displayed under \"Available config options\" when -starting a new shell, for example \"low\", \"high\" or \"max\". - -Variants are model-specific, so this is applied after -`agent-shell-opencode-default-model-id'. It is ignored for models -offering no variants." - :type '(choice (const nil) string) + "Default OpenCode config options, applied at session start. + +An alist of (OPTION . VALUE). Both are the ids listed under +\"Available config options\" when starting a new shell, so whatever +OpenCode advertises can be set here without further agent-shell +changes. OPTION also accepts the ACP category names (\"model\", +\"mode\", \"thought_level\") for options carrying one. + +OpenCode exposes model variants (provider-specific reasoning effort) +as its \"effort\" option: + + (setq agent-shell-opencode-default-config-options + \\='((\"model\" . \"anthropic/claude-opus-4-5\") + (\"effort\" . \"high\") + (\"mode\" . \"plan\"))) + +Options are applied in the order listed. Order matters: OpenCode +scopes the available efforts to the active model, so \"effort\" belongs +after \"model\". An option or value OpenCode does not offer is +reported and skipped." + :type '(alist :key-type string :value-type string) :group 'agent-shell) (defcustom agent-shell-opencode-default-session-mode-id @@ -143,7 +154,7 @@ Returns an agent configuration alist using `agent-shell-make-agent-config'." (agent-shell-opencode-make-client :buffer buffer)) :default-model-id (lambda () agent-shell-opencode-default-model-id) :default-session-mode-id (lambda () agent-shell-opencode-default-session-mode-id) - :default-thought-level-id (lambda () agent-shell-opencode-default-model-variant) + :default-config-options (lambda () agent-shell-opencode-default-config-options) :install-instructions "See https://opencode.ai/docs for installation.")) ;;;###autoload diff --git a/tests/agent-shell-opencode-tests.el b/tests/agent-shell-opencode-tests.el index d5e7f350..4545d3a8 100644 --- a/tests/agent-shell-opencode-tests.el +++ b/tests/agent-shell-opencode-tests.el @@ -17,16 +17,20 @@ (let ((agent-shell-opencode-default-model-id "anthropic/claude-opus-4-5")) (should (string= (funcall default-model-id-fn) "anthropic/claude-opus-4-5"))))) -(ert-deftest agent-shell-opencode-default-model-variant-test () - "Test that OpenCode config exposes default model variant as thought level." - (let ((default-thought-level-id-fn - (map-elt (agent-shell-opencode-make-agent-config) :default-thought-level-id))) - - (let ((agent-shell-opencode-default-model-variant nil)) - (should (null (funcall default-thought-level-id-fn)))) - - (let ((agent-shell-opencode-default-model-variant "high")) - (should (string= (funcall default-thought-level-id-fn) "high"))))) +(ert-deftest agent-shell-opencode-default-config-options-test () + "Test that OpenCode config exposes default config options." + (let ((default-config-options-fn + (map-elt (agent-shell-opencode-make-agent-config) :default-config-options))) + + (let ((agent-shell-opencode-default-config-options nil)) + (should (null (funcall default-config-options-fn)))) + + (let ((agent-shell-opencode-default-config-options + '(("model" . "anthropic/claude-opus-4-5") + ("effort" . "high")))) + (should (equal (funcall default-config-options-fn) + '(("model" . "anthropic/claude-opus-4-5") + ("effort" . "high"))))))) (ert-deftest agent-shell-opencode-default-session-mode-id-test () "Test that OpenCode config exposes default session mode id." From ea4d4681716c19bce709be14e5bf9073c8a52570 Mon Sep 17 00:00:00 2001 From: John Buckley Date: Tue, 25 Aug 2026 14:00:33 +0100 Subject: [PATCH 5/5] Delegate model and mode config options to their dedicated setters agent-shell--config-option-set-model-id and its mode twin fall back to session/set_model and session/set_mode for agents advertising no config options, so default-config-options routes through them rather than always sending session/set_config_option. Dispatch is on the entry's key, not the resolved option's category: Cline tags both its provider and model options with category "model", so routing on category would hand a provider entry to the model setter, which resolves category "model" back to the model option. Co-Authored-By: Claude Opus 5 --- agent-shell-config.el | 38 ---------- agent-shell.el | 137 ++++++++++++++++++++++++++++++------- tests/agent-shell-tests.el | 125 +++++++++++++++++++++++++++++---- 3 files changed, 225 insertions(+), 75 deletions(-) diff --git a/agent-shell-config.el b/agent-shell-config.el index 5620b89e..4206f230 100644 --- a/agent-shell-config.el +++ b/agent-shell-config.el @@ -164,44 +164,6 @@ categorized as \"thought_level\": (or (agent-shell--config-option-get :state state :id option) (agent-shell--config-option-by-category state option))) -(defun agent-shell--config-option-offers-value-p (option value) - "Return non-nil when OPTION offers VALUE. - -Only options enumerating their values can be checked, so an option -advertising none (a free-form string option, for example) accepts any -VALUE. - -For example: - - (agent-shell--config-option-offers-value-p - \\='((:options . (((:value . \"low\")) ((:value . \"high\"))))) \"high\") - => t" - (or (seq-empty-p (map-elt option :options)) - (seq-find (lambda (candidate) - (equal value (map-elt candidate :value))) - (map-elt option :options)))) - -(defun agent-shell--config-option-skip-reason (state option) - "Explain why OPTION could not be set in STATE. - -Names the ids the agent does offer, since agents advertise options -conditionally and scope their values to the active model. - -For example: - - (agent-shell--config-option-skip-reason state \"effort\") - => \"agent offers low, high, max\" - - (agent-shell--config-option-skip-reason state \"fast\") - => \"agent advertises no fast option\"" - (if-let* ((resolved (agent-shell--resolve-config-option state option))) - (format "agent offers %s" - (string-join (seq-map (lambda (candidate) - (map-elt candidate :value)) - (map-elt resolved :options)) - ", ")) - (format "agent advertises no %s option" option))) - (defun agent-shell--select-config-options (state) "Return selectable (type = \"select\") config options from STATE." (seq-filter (lambda (option) diff --git a/agent-shell.el b/agent-shell.el index 01a8d801..6b34b76a 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -733,10 +733,14 @@ Keyword arguments: - DEFAULT-CONFIG-OPTIONS: Default ACP session config options (function returning an alist of (OPTION . VALUE), both strings). OPTION is matched against the ids the agent advertises, falling back to ACP - categories (\"model\", \"mode\", \"thought_level\"). Applied in the - order listed, after DEFAULT-MODEL-ID and DEFAULT-SESSION-MODE-ID. - Order matters: options an agent scopes to the active model (thought - level, for example) must follow the option selecting that model. + categories (\"model\", \"mode\", \"thought_level\"). The categories + \"model\" and \"mode\" additionally reach agents advertising no config + options, via the same legacy requests DEFAULT-MODEL-ID and + DEFAULT-SESSION-MODE-ID use. Applied in the order listed, after + DEFAULT-MODEL-ID and DEFAULT-SESSION-MODE-ID, so an entry here wins + over either. Order matters: options an agent scopes to the active + model (thought level, for example) must follow the option selecting + that model. - SESSION-META: Optional alist of agent-specific metadata sent as `_meta' with session-creating requests (`session/new', `session/load', `session/resume', and `session/fork'). @@ -6637,6 +6641,67 @@ Call ON-MODE-CHANGED on success." :on-failure (agent-shell--make-error-handler :state (agent-shell--state) :shell-buffer shell-buffer)))) +(defun agent-shell--default-config-option-values (state option) + "Return the value ids STATE advertises for OPTION. + +The ACP categories \"model\" and \"mode\" read through the accessors +that unify config options with the legacy `models'/`modes' session +fields, so they cover agents advertising no config options at all. +Returns nil when OPTION is unknown to STATE, or constrains nothing. + +For example: + + (agent-shell--default-config-option-values state \"thought_level\") + => \\='(\"low\" \"high\" \"max\")" + (pcase option + ("model" (seq-map (lambda (model) + (map-elt model :model-id)) + (agent-shell--get-available-models state))) + ("mode" (seq-map (lambda (mode) + (map-elt mode :id)) + (agent-shell--get-available-modes state))) + (_ (seq-map (lambda (value) + (map-elt value :value)) + (map-elt (agent-shell--resolve-config-option state option) :options))))) + +(defun agent-shell--default-config-option-addressable-p (state option) + "Return non-nil when STATE can be asked to set OPTION. + +\"model\" and \"mode\" are always addressable: agents advertising no +config options still answer the legacy `session/set_model' and +`session/set_mode' requests. Any other OPTION has to resolve to an +advertised config option." + (or (member option '("model" "mode")) + (agent-shell--resolve-config-option state option))) + +(defun agent-shell--default-config-option-settable-p (state option value) + "Return non-nil when STATE can be asked to set OPTION to VALUE. + +An option enumerating no values (a free-form string option, or one an +agent only exposes over the legacy requests) accepts any VALUE." + (and (agent-shell--default-config-option-addressable-p state option) + (if-let* ((values (agent-shell--default-config-option-values state option))) + (member value values) + t))) + +(defun agent-shell--default-config-option-skip-reason (state option) + "Explain why OPTION could not be set in STATE. + +Names the ids the agent does offer, since agents advertise options +conditionally and scope their values to the active model. + +For example: + + (agent-shell--default-config-option-skip-reason state \"effort\") + => \"agent offers low, high, max\" + + (agent-shell--default-config-option-skip-reason state \"fast\") + => \"agent advertises no fast option\"" + (if-let* (((agent-shell--default-config-option-addressable-p state option)) + (values (agent-shell--default-config-option-values state option))) + (format "agent offers %s" (string-join values ", ")) + (format "agent advertises no %s option" option))) + (cl-defun agent-shell--set-default-config-options (&key shell-buffer config-options (first t) on-options-set) "Apply CONFIG-OPTIONS in SHELL-BUFFER, one at a time, in order. @@ -6668,11 +6733,10 @@ Call ON-OPTIONS-SET once the list is exhausted." (cl-defun agent-shell--set-default-config-option (&key shell-buffer option value first on-option-set) "Set config OPTION to VALUE in SHELL-BUFFER, then call ON-OPTION-SET. -OPTION is matched against advertised option ids first, then ACP -categories. Agents advertise options conditionally (thought levels -only for models supporting them, for example) and scope values to the -active model, so an unknown option or value is reported and skipped -rather than aborting initialization. +Agents advertise options conditionally (thought levels only for models +supporting them, for example) and scope values to the active model, so +an unknown option or value is reported and skipped rather than +aborting initialization. FIRST reports this as the opening line of the shared progress block, which later entries append their own line to." @@ -6684,30 +6748,55 @@ which later entries append their own line to." :label-left (propertize "Setting config options" 'font-lock-face 'agent-shell-section-heading) :body (format "%s%s: requesting %s..." (if first "" "\n") option value) :append t)) - (if-let* ((resolved (agent-shell--resolve-config-option (agent-shell--state) option)) - ((agent-shell--config-option-offers-value-p resolved value))) - (agent-shell--set-session-config-option - :config-id (map-elt resolved :id) + (if (agent-shell--default-config-option-settable-p (agent-shell--state) option value) + (agent-shell--send-default-config-option + :shell-buffer shell-buffer + :option option :value value - :on-success (lambda () - (agent-shell--update-bootstrapping-fragment - :state (agent-shell--state) - :block-id "set-config-options" - :body " done" - :append t) - (when on-option-set - (funcall on-option-set))) - :on-failure (agent-shell--make-error-handler - :state (agent-shell--state) :shell-buffer shell-buffer)) + :on-sent (lambda () + (agent-shell--update-bootstrapping-fragment + :state (agent-shell--state) + :block-id "set-config-options" + :body " done" + :append t) + (when on-option-set + (funcall on-option-set)))) (agent-shell--update-bootstrapping-fragment :state (agent-shell--state) :block-id "set-config-options" :body (format " skipped (%s)" - (agent-shell--config-option-skip-reason (agent-shell--state) option)) + (agent-shell--default-config-option-skip-reason (agent-shell--state) option)) :append t) (when on-option-set (funcall on-option-set))))) +(cl-defun agent-shell--send-default-config-option (&key shell-buffer option value on-sent) + "Ask the agent to set OPTION to VALUE, then call ON-SENT. + +The ACP categories \"model\" and \"mode\" route through the setters +owning their legacy fallbacks, so an agent advertising no config +options is still reachable over `session/set_model' and +`session/set_mode'. Any other OPTION resolves to an advertised +config option and goes out as `session/set_config_option'. + +SHELL-BUFFER is where a rejected request reports its error." + (let ((on-failure (agent-shell--make-error-handler + :state (agent-shell--state) :shell-buffer shell-buffer))) + (pcase option + ("model" (agent-shell--config-option-set-model-id + :model-id value + :on-success on-sent + :on-failure on-failure)) + ("mode" (agent-shell--config-option-set-mode-id + :mode-id value + :on-success on-sent + :on-failure on-failure)) + (_ (agent-shell--set-session-config-option + :config-id (map-elt (agent-shell--resolve-config-option (agent-shell--state) option) :id) + :value value + :on-success on-sent + :on-failure on-failure))))) + (cl-defun agent-shell--initiate-session (&key shell-buffer on-session-init) "Initiate ACP session creation with SHELL-BUFFER. diff --git a/tests/agent-shell-tests.el b/tests/agent-shell-tests.el index ae319664..51475a4c 100644 --- a/tests/agent-shell-tests.el +++ b/tests/agent-shell-tests.el @@ -1566,23 +1566,122 @@ OpenCode names its thought level option `effort', so the spec's (should (equal (map-elt (agent-shell--resolve-config-option state "model") :id) "model")))) -(ert-deftest agent-shell--config-option-offers-value-test () - "Test `agent-shell--config-option-offers-value-p'." - (let ((effort (agent-shell--resolve-config-option - (agent-shell-tests--opencode-config-options-state) "effort"))) - (should (agent-shell--config-option-offers-value-p effort "high")) - (should-not (agent-shell--config-option-offers-value-p effort "turbo"))) - ;; An option enumerating no values accepts anything. - (should (agent-shell--config-option-offers-value-p '((:options . nil)) "anything"))) - -(ert-deftest agent-shell--config-option-skip-reason-test () - "Test `agent-shell--config-option-skip-reason'." +(ert-deftest agent-shell--default-config-option-values-test () + "Test `agent-shell--default-config-option-values'." (let ((state (agent-shell-tests--opencode-config-options-state))) - (should (equal (agent-shell--config-option-skip-reason state "effort") + (should (equal (agent-shell--default-config-option-values state "effort") + '("low" "high" "max"))) + (should (equal (agent-shell--default-config-option-values state "thought_level") + '("low" "high" "max"))) + (should (equal (agent-shell--default-config-option-values state "model") + '("anthropic/claude-opus-4-5"))) + (should-not (agent-shell--default-config-option-values state "reasoning")))) + +(ert-deftest agent-shell--default-config-option-settable-test () + "Test `agent-shell--default-config-option-settable-p'." + (let ((state (agent-shell-tests--opencode-config-options-state))) + (should (agent-shell--default-config-option-settable-p state "effort" "high")) + (should-not (agent-shell--default-config-option-settable-p state "effort" "turbo")) + (should-not (agent-shell--default-config-option-settable-p state "reasoning" "high"))) + ;; "model" and "mode" stay settable with no config options advertised, + ;; since the legacy requests still reach the agent. + (let ((state (list (cons :session (list (cons :id "session-1")))))) + (should (agent-shell--default-config-option-settable-p state "model" "gpt-5.5")) + (should (agent-shell--default-config-option-settable-p state "mode" "plan")) + (should-not (agent-shell--default-config-option-settable-p state "effort" "high")))) + +(ert-deftest agent-shell--default-config-option-skip-reason-test () + "Test `agent-shell--default-config-option-skip-reason'." + (let ((state (agent-shell-tests--opencode-config-options-state))) + (should (equal (agent-shell--default-config-option-skip-reason state "effort") "agent offers low, high, max")) - (should (equal (agent-shell--config-option-skip-reason state "reasoning") + (should (equal (agent-shell--default-config-option-skip-reason state "reasoning") "agent advertises no reasoning option")))) +(ert-deftest agent-shell--set-default-config-option-legacy-model-test () + "Test a \"model\" entry falls back to the legacy ACP model request. + +An agent advertising no config options still answers +`session/set_model', so `agent-shell--set-default-config-options' has +to reach it the same way `:default-model-id' does." + (let ((sent-request nil) + (finished nil)) + (with-temp-buffer + (setq-local agent-shell--state + (list (cons :buffer (current-buffer)) + (cons :client 'test-client) + (cons :session (list (cons :id "session-1") + (cons :model-id "gpt-5") + (cons :models '(((:model-id . "gpt-5")) + ((:model-id . "gpt-5.5")))))))) + (cl-letf (((symbol-function 'agent-shell--state) + (lambda () agent-shell--state)) + ((symbol-function 'agent-shell--update-bootstrapping-fragment) + (lambda (&rest _))) + ((symbol-function 'agent-shell--update-header-and-mode-line) + #'ignore) + ((symbol-function 'agent-shell--emit-event) + (lambda (&rest _))) + ((symbol-function 'agent-shell--send-request) + (lambda (&rest args) + (setq sent-request (plist-get args :request)) + (funcall (plist-get args :on-success) nil)))) + (agent-shell--set-default-config-options + :shell-buffer (current-buffer) + :config-options '(("model" . "gpt-5.5")) + :on-options-set (lambda () (setq finished t))))) + (should (equal (map-elt sent-request :method) "session/set_model")) + (should (equal (map-nested-elt sent-request '(:params modelId)) "gpt-5.5")) + (should finished))) + +(ert-deftest agent-shell--set-default-config-option-sibling-category-test () + "Test an entry naming a sibling of \"model\" is not routed as the model. + +Cline tags both its `provider' and `model' options with category +\"model\", so routing on the resolved option's category would send a +`provider' entry to the model setter, which resolves category +\"model\" back to the model option." + (let ((sent nil)) + (with-temp-buffer + (setq-local agent-shell--state + (list (cons :buffer (current-buffer)) + (cons :session (list (cons :id "session-1"))) + (cons :config-options + (agent-shell--normalize-config-options + [((id . "provider") + (name . "Provider") + (category . "model") + (type . "select") + (currentValue . "openai-codex") + (options . [((value . "cline") (name . "Cline")) + ((value . "openai-codex") (name . "Codex"))])) + ((id . "model") + (name . "Model") + (category . "model") + (type . "select") + (currentValue . "gpt-5.5") + (options . [((value . "gpt-5.5") (name . "GPT-5.5"))]))])))) + (cl-letf (((symbol-function 'agent-shell--state) + (lambda () agent-shell--state)) + ((symbol-function 'agent-shell--update-bootstrapping-fragment) + (lambda (&rest _))) + ((symbol-function 'agent-shell--emit-event) + (lambda (&rest _))) + ((symbol-function 'agent-shell--set-session-config-option) + (lambda (&rest args) + (push (cons (plist-get args :config-id) + (plist-get args :value)) + sent) + (funcall (plist-get args :on-success))))) + (agent-shell--set-default-config-options + :shell-buffer (current-buffer) + :config-options '(("provider" . "cline") + ("model" . "gpt-5.5")) + :on-options-set #'ignore))) + (should (equal (reverse sent) + '(("provider" . "cline") + ("model" . "gpt-5.5")))))) + (ert-deftest agent-shell--set-default-config-options-test () "Test `agent-shell--set-default-config-options' applies options in order.