Skip to content
Merged
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
51 changes: 49 additions & 2 deletions pi-coding-agent-menu.el
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
;; `pi-coding-agent-resume-session' Resume previous session
;; `pi-coding-agent-reload' Restart pi process
;; `pi-coding-agent-select-model' Choose model interactively
;; `pi-coding-agent-cycle-thinking' Cycle thinking levels
;; `pi-coding-agent-select-thinking' Choose thinking level interactively
;; `pi-coding-agent-cycle-thinking' Cycle thinking levels from header-line
;; `pi-coding-agent-compact' Compact conversation context
;; `pi-coding-agent-fork' Fork from previous message

Expand Down Expand Up @@ -524,6 +525,11 @@ Optional INITIAL-INPUT pre-fills the completion prompt for filtering."
(force-mode-line-update))
(message "Pi: Model set to %s" choice)))))))))

(defconst pi-coding-agent--thinking-levels '("off" "minimal" "low" "medium" "high" "xhigh")
"Thinking levels accepted by `set_thinking_level' RPC.

Unsupported levels are clamped to the current model's capabilities.")

(defun pi-coding-agent-cycle-thinking ()
"Cycle through thinking levels."
(interactive)
Expand All @@ -539,6 +545,47 @@ Optional INITIAL-INPUT pre-fills the completion prompt for filtering."
(message "Pi: Thinking level: %s"
(plist-get pi-coding-agent--state :thinking-level))))))))

(defun pi-coding-agent--refresh-thinking-level-state (proc chat-buf)
"Refresh CHAT-BUF state from PROC after a thinking-level change.
Uses `get_state' so the UI reflects the server's actual level,
including any model-specific clamping."
(pi-coding-agent--rpc-async
proc '(:type "get_state")
(lambda (response)
(if (plist-get response :success)
(let* ((data (plist-get response :data))
(level (or (plist-get data :thinkingLevel) "off")))
(pi-coding-agent--apply-state-response chat-buf response)
(message "Pi: Thinking level: %s" level))
(message "Pi: Thinking level updated, but failed to refresh state%s"
(if-let* ((error-text (plist-get response :error)))
(format ": %s" error-text)
""))))))

(defun pi-coding-agent-select-thinking ()
"Select a thinking level from the minibuffer."
(interactive)
(let ((proc (pi-coding-agent--get-process))
(chat-buf (pi-coding-agent--get-chat-buffer)))
(unless proc
(user-error "No pi process running"))
(unless chat-buf
(user-error "No pi session buffer"))
(let* ((state (pi-coding-agent--menu-state))
(current (or (plist-get state :thinking-level) "off"))
(choice (completing-read
(format "Thinking level (current: %s): " current)
pi-coding-agent--thinking-levels
nil t)))
(unless (equal choice current)
(pi-coding-agent--rpc-async
proc (list :type "set_thinking_level" :level choice)
(lambda (response)
(if (plist-get response :success)
(pi-coding-agent--refresh-thinking-level-state proc chat-buf)
(message "Pi: Failed to set thinking level: %s"
(or (plist-get response :error) "unknown error")))))))))

;;;; Session Info and Actions

(defun pi-coding-agent--format-session-stats (stats)
Expand Down Expand Up @@ -900,7 +947,7 @@ Uses commands from pi's `get_commands' RPC."
("f" "fork" pi-coding-agent-fork)]]
[["Model"
("m" "select" pi-coding-agent-select-model)
("t" "thinking" pi-coding-agent-cycle-thinking)]
("t" "thinking" pi-coding-agent-select-thinking)]
["Info"
("i" "stats" pi-coding-agent-session-stats)
("y" "copy last" pi-coding-agent-copy-last-message)]]
Expand Down
13 changes: 9 additions & 4 deletions test/pi-coding-agent-input-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -2314,18 +2314,23 @@ Pi handles command expansion on the server side."
(should (get-text-property 0 'mouse-face header)))))

(ert-deftest pi-coding-agent-test-header-line-thinking-is-clickable ()
"Thinking level in header-line has click properties."
"Thinking level in header-line cycles on mouse click."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--state '(:model (:name "test") :thinking-level "high"))
(let* ((header (pi-coding-agent--header-line-string))
;; Find position of "high" in header
(pos (string-match "high" header)))
(pos (string-match "high" header))
(map (and pos (get-text-property pos 'local-map header))))
(should pos)
;; Should have local-map at that position
(should (get-text-property pos 'local-map header))
(should map)
;; Should have mouse-face for highlight
(should (get-text-property pos 'mouse-face header)))))
(should (get-text-property pos 'mouse-face header))
(should (eq (lookup-key map [header-line mouse-1])
#'pi-coding-agent-cycle-thinking))
(should (eq (lookup-key map [header-line mouse-2])
#'pi-coding-agent-cycle-thinking)))))

(ert-deftest pi-coding-agent-test-header-format-context-returns-nil-when-no-window ()
"Context format returns nil when context window is 0."
Expand Down
130 changes: 130 additions & 0 deletions test/pi-coding-agent-menu-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,136 @@ Regression: when called from input buffer, state is nil → \"unknown\"."
(should completing-read-called)
(should (equal captured-initial "opus"))))

(ert-deftest pi-coding-agent-test-select-thinking-refreshes-state-from-server ()
"Thinking selector refreshes state so server clamping is visible in the UI."
(let (captured-prompt captured-collection rpc-commands last-message)
(with-temp-buffer
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--process :fake-proc
pi-coding-agent--state '(:thinking-level "low"))
(cl-letf (((symbol-function 'completing-read)
(lambda (prompt collection &rest _)
(setq captured-prompt prompt
captured-collection collection)
"high"))
((symbol-function 'pi-coding-agent--rpc-async)
(lambda (_proc cmd callback)
(push cmd rpc-commands)
(pcase (plist-get cmd :type)
("set_thinking_level"
(funcall callback '(:success t :command "set_thinking_level")))
("get_state"
(funcall callback
'(:success t
:data (:thinkingLevel "medium"
:isStreaming nil
:isCompacting nil)))))))
((symbol-function 'message)
(lambda (fmt &rest args)
(setq last-message (apply #'format fmt args)))))
(pi-coding-agent-select-thinking)
(should (equal (plist-get pi-coding-agent--state :thinking-level) "medium"))))
(should (equal captured-prompt "Thinking level (current: low): "))
(should (equal captured-collection
'("off" "minimal" "low" "medium" "high" "xhigh")))
(let ((commands (nreverse rpc-commands)))
(should (equal (mapcar (lambda (cmd) (plist-get cmd :type)) commands)
'("set_thinking_level" "get_state")))
(should (equal (car commands)
'(:type "set_thinking_level" :level "high")))
(should (equal (cadr commands) '(:type "get_state"))))
(should (equal last-message "Pi: Thinking level: medium"))))

(ert-deftest pi-coding-agent-test-select-thinking-noop-when-unchanged ()
"Thinking selector does not send RPC when the user picks the current level."
(let (rpc-called)
(with-temp-buffer
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--process :fake-proc
pi-coding-agent--state '(:thinking-level "medium"))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "medium"))
((symbol-function 'pi-coding-agent--rpc-async)
(lambda (&rest _)
(setq rpc-called t))))
(pi-coding-agent-select-thinking)))
(should-not rpc-called)))

(ert-deftest pi-coding-agent-test-select-thinking-errors-without-process ()
"Thinking selector should fail loudly when no pi process is running."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(should-error (pi-coding-agent-select-thinking) :type 'user-error)))

(ert-deftest pi-coding-agent-test-select-thinking-shows-rpc-error ()
"Thinking selector reports set_thinking_level RPC failures."
(let (rpc-commands shown-message)
(with-temp-buffer
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--process :fake-proc
pi-coding-agent--state '(:thinking-level "low"))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "high"))
((symbol-function 'pi-coding-agent--rpc-async)
(lambda (_proc cmd callback)
(push cmd rpc-commands)
(funcall callback '(:success nil :error "unsupported"))))
((symbol-function 'message)
(lambda (fmt &rest args)
(setq shown-message (apply #'format fmt args)))))
(pi-coding-agent-select-thinking)
(should (equal (plist-get pi-coding-agent--state :thinking-level) "low"))))
(should (equal rpc-commands
'((:type "set_thinking_level" :level "high"))))
(should (equal shown-message
"Pi: Failed to set thinking level: unsupported"))))

(ert-deftest pi-coding-agent-test-select-thinking-warns-when-state-refresh-fails ()
"Thinking selector warns instead of guessing when state refresh fails."
(let (shown-message)
(with-temp-buffer
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--process :fake-proc
pi-coding-agent--state '(:thinking-level "low"))
(cl-letf (((symbol-function 'completing-read)
(lambda (&rest _) "high"))
((symbol-function 'pi-coding-agent--rpc-async)
(lambda (_proc cmd callback)
(pcase (plist-get cmd :type)
("set_thinking_level"
(funcall callback '(:success t :command "set_thinking_level")))
("get_state"
(funcall callback '(:success nil :error "state unavailable"))))))
((symbol-function 'message)
(lambda (fmt &rest args)
(setq shown-message (apply #'format fmt args)))))
(pi-coding-agent-select-thinking)
(should (equal (plist-get pi-coding-agent--state :thinking-level) "low"))))
(should (equal shown-message
"Pi: Thinking level updated, but failed to refresh state: state unavailable"))))

(ert-deftest pi-coding-agent-test-thinking-selector-uses-t-key-leaving-T-for-templates ()
"Main menu binds `t' to thinking selection without taking Templates `T'."
(let ((pi-coding-agent--commands
'((:name "review" :description "Code review" :source "prompt"))))
(unwind-protect
(progn
(pi-coding-agent--rebuild-commands-menu)
(transient-setup 'pi-coding-agent-menu)
(let ((thinking-suffix
(cl-find-if (lambda (obj)
(equal (oref obj key) "t"))
transient--suffixes))
(templates-suffix
(cl-find-if (lambda (obj)
(equal (oref obj key) "T"))
transient--suffixes)))
(should thinking-suffix)
(should (eq (oref thinking-suffix command)
'pi-coding-agent-select-thinking))
(should templates-suffix)))
(ignore-errors (transient-remove-suffix 'pi-coding-agent-menu '(4))))))

;;; sourceInfo normalization

(ert-deftest pi-coding-agent-test-normalize-command-extracts-source-info ()
Expand Down
Loading