Skip to content

Commit 3461ba9

Browse files
dnouriDevGiuDev
andauthored
Let users choose the thinking level from the menu (#191)
The `t` entry in the transient menu now opens a minibuffer selector, so users can jump straight to the thinking level they want instead of cycling through every level. Header-line clicks still cycle levels for quick adjustments. After changing the level, Emacs now refreshes state from pi and shows the level that pi actually accepted. This matters when a model clamps unsupported levels. The command also reports missing processes and RPC failures instead of failing silently, and the new tests cover both the normal and error paths. Co-authored-by: DevGiu <[email protected]>
1 parent 8908e60 commit 3461ba9

3 files changed

Lines changed: 188 additions & 6 deletions

File tree

pi-coding-agent-menu.el

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
;; `pi-coding-agent-resume-session' Resume previous session
3535
;; `pi-coding-agent-reload' Restart pi process
3636
;; `pi-coding-agent-select-model' Choose model interactively
37-
;; `pi-coding-agent-cycle-thinking' Cycle thinking levels
37+
;; `pi-coding-agent-select-thinking' Choose thinking level interactively
38+
;; `pi-coding-agent-cycle-thinking' Cycle thinking levels from header-line
3839
;; `pi-coding-agent-compact' Compact conversation context
3940
;; `pi-coding-agent-fork' Fork from previous message
4041

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

528+
(defconst pi-coding-agent--thinking-levels '("off" "minimal" "low" "medium" "high" "xhigh")
529+
"Thinking levels accepted by `set_thinking_level' RPC.
530+
531+
Unsupported levels are clamped to the current model's capabilities.")
532+
527533
(defun pi-coding-agent-cycle-thinking ()
528534
"Cycle through thinking levels."
529535
(interactive)
@@ -539,6 +545,47 @@ Optional INITIAL-INPUT pre-fills the completion prompt for filtering."
539545
(message "Pi: Thinking level: %s"
540546
(plist-get pi-coding-agent--state :thinking-level))))))))
541547

548+
(defun pi-coding-agent--refresh-thinking-level-state (proc chat-buf)
549+
"Refresh CHAT-BUF state from PROC after a thinking-level change.
550+
Uses `get_state' so the UI reflects the server's actual level,
551+
including any model-specific clamping."
552+
(pi-coding-agent--rpc-async
553+
proc '(:type "get_state")
554+
(lambda (response)
555+
(if (plist-get response :success)
556+
(let* ((data (plist-get response :data))
557+
(level (or (plist-get data :thinkingLevel) "off")))
558+
(pi-coding-agent--apply-state-response chat-buf response)
559+
(message "Pi: Thinking level: %s" level))
560+
(message "Pi: Thinking level updated, but failed to refresh state%s"
561+
(if-let* ((error-text (plist-get response :error)))
562+
(format ": %s" error-text)
563+
""))))))
564+
565+
(defun pi-coding-agent-select-thinking ()
566+
"Select a thinking level from the minibuffer."
567+
(interactive)
568+
(let ((proc (pi-coding-agent--get-process))
569+
(chat-buf (pi-coding-agent--get-chat-buffer)))
570+
(unless proc
571+
(user-error "No pi process running"))
572+
(unless chat-buf
573+
(user-error "No pi session buffer"))
574+
(let* ((state (pi-coding-agent--menu-state))
575+
(current (or (plist-get state :thinking-level) "off"))
576+
(choice (completing-read
577+
(format "Thinking level (current: %s): " current)
578+
pi-coding-agent--thinking-levels
579+
nil t)))
580+
(unless (equal choice current)
581+
(pi-coding-agent--rpc-async
582+
proc (list :type "set_thinking_level" :level choice)
583+
(lambda (response)
584+
(if (plist-get response :success)
585+
(pi-coding-agent--refresh-thinking-level-state proc chat-buf)
586+
(message "Pi: Failed to set thinking level: %s"
587+
(or (plist-get response :error) "unknown error")))))))))
588+
542589
;;;; Session Info and Actions
543590

544591
(defun pi-coding-agent--format-session-stats (stats)
@@ -900,7 +947,7 @@ Uses commands from pi's `get_commands' RPC."
900947
("f" "fork" pi-coding-agent-fork)]]
901948
[["Model"
902949
("m" "select" pi-coding-agent-select-model)
903-
("t" "thinking" pi-coding-agent-cycle-thinking)]
950+
("t" "thinking" pi-coding-agent-select-thinking)]
904951
["Info"
905952
("i" "stats" pi-coding-agent-session-stats)
906953
("y" "copy last" pi-coding-agent-copy-last-message)]]

test/pi-coding-agent-input-test.el

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2314,18 +2314,23 @@ Pi handles command expansion on the server side."
23142314
(should (get-text-property 0 'mouse-face header)))))
23152315

23162316
(ert-deftest pi-coding-agent-test-header-line-thinking-is-clickable ()
2317-
"Thinking level in header-line has click properties."
2317+
"Thinking level in header-line cycles on mouse click."
23182318
(with-temp-buffer
23192319
(pi-coding-agent-chat-mode)
23202320
(setq pi-coding-agent--state '(:model (:name "test") :thinking-level "high"))
23212321
(let* ((header (pi-coding-agent--header-line-string))
23222322
;; Find position of "high" in header
2323-
(pos (string-match "high" header)))
2323+
(pos (string-match "high" header))
2324+
(map (and pos (get-text-property pos 'local-map header))))
23242325
(should pos)
23252326
;; Should have local-map at that position
2326-
(should (get-text-property pos 'local-map header))
2327+
(should map)
23272328
;; Should have mouse-face for highlight
2328-
(should (get-text-property pos 'mouse-face header)))))
2329+
(should (get-text-property pos 'mouse-face header))
2330+
(should (eq (lookup-key map [header-line mouse-1])
2331+
#'pi-coding-agent-cycle-thinking))
2332+
(should (eq (lookup-key map [header-line mouse-2])
2333+
#'pi-coding-agent-cycle-thinking)))))
23292334

23302335
(ert-deftest pi-coding-agent-test-header-format-context-returns-nil-when-no-window ()
23312336
"Context format returns nil when context window is 0."

test/pi-coding-agent-menu-test.el

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,6 +1259,136 @@ Regression: when called from input buffer, state is nil → \"unknown\"."
12591259
(should completing-read-called)
12601260
(should (equal captured-initial "opus"))))
12611261

1262+
(ert-deftest pi-coding-agent-test-select-thinking-refreshes-state-from-server ()
1263+
"Thinking selector refreshes state so server clamping is visible in the UI."
1264+
(let (captured-prompt captured-collection rpc-commands last-message)
1265+
(with-temp-buffer
1266+
(pi-coding-agent-chat-mode)
1267+
(setq pi-coding-agent--process :fake-proc
1268+
pi-coding-agent--state '(:thinking-level "low"))
1269+
(cl-letf (((symbol-function 'completing-read)
1270+
(lambda (prompt collection &rest _)
1271+
(setq captured-prompt prompt
1272+
captured-collection collection)
1273+
"high"))
1274+
((symbol-function 'pi-coding-agent--rpc-async)
1275+
(lambda (_proc cmd callback)
1276+
(push cmd rpc-commands)
1277+
(pcase (plist-get cmd :type)
1278+
("set_thinking_level"
1279+
(funcall callback '(:success t :command "set_thinking_level")))
1280+
("get_state"
1281+
(funcall callback
1282+
'(:success t
1283+
:data (:thinkingLevel "medium"
1284+
:isStreaming nil
1285+
:isCompacting nil)))))))
1286+
((symbol-function 'message)
1287+
(lambda (fmt &rest args)
1288+
(setq last-message (apply #'format fmt args)))))
1289+
(pi-coding-agent-select-thinking)
1290+
(should (equal (plist-get pi-coding-agent--state :thinking-level) "medium"))))
1291+
(should (equal captured-prompt "Thinking level (current: low): "))
1292+
(should (equal captured-collection
1293+
'("off" "minimal" "low" "medium" "high" "xhigh")))
1294+
(let ((commands (nreverse rpc-commands)))
1295+
(should (equal (mapcar (lambda (cmd) (plist-get cmd :type)) commands)
1296+
'("set_thinking_level" "get_state")))
1297+
(should (equal (car commands)
1298+
'(:type "set_thinking_level" :level "high")))
1299+
(should (equal (cadr commands) '(:type "get_state"))))
1300+
(should (equal last-message "Pi: Thinking level: medium"))))
1301+
1302+
(ert-deftest pi-coding-agent-test-select-thinking-noop-when-unchanged ()
1303+
"Thinking selector does not send RPC when the user picks the current level."
1304+
(let (rpc-called)
1305+
(with-temp-buffer
1306+
(pi-coding-agent-chat-mode)
1307+
(setq pi-coding-agent--process :fake-proc
1308+
pi-coding-agent--state '(:thinking-level "medium"))
1309+
(cl-letf (((symbol-function 'completing-read)
1310+
(lambda (&rest _) "medium"))
1311+
((symbol-function 'pi-coding-agent--rpc-async)
1312+
(lambda (&rest _)
1313+
(setq rpc-called t))))
1314+
(pi-coding-agent-select-thinking)))
1315+
(should-not rpc-called)))
1316+
1317+
(ert-deftest pi-coding-agent-test-select-thinking-errors-without-process ()
1318+
"Thinking selector should fail loudly when no pi process is running."
1319+
(with-temp-buffer
1320+
(pi-coding-agent-chat-mode)
1321+
(should-error (pi-coding-agent-select-thinking) :type 'user-error)))
1322+
1323+
(ert-deftest pi-coding-agent-test-select-thinking-shows-rpc-error ()
1324+
"Thinking selector reports set_thinking_level RPC failures."
1325+
(let (rpc-commands shown-message)
1326+
(with-temp-buffer
1327+
(pi-coding-agent-chat-mode)
1328+
(setq pi-coding-agent--process :fake-proc
1329+
pi-coding-agent--state '(:thinking-level "low"))
1330+
(cl-letf (((symbol-function 'completing-read)
1331+
(lambda (&rest _) "high"))
1332+
((symbol-function 'pi-coding-agent--rpc-async)
1333+
(lambda (_proc cmd callback)
1334+
(push cmd rpc-commands)
1335+
(funcall callback '(:success nil :error "unsupported"))))
1336+
((symbol-function 'message)
1337+
(lambda (fmt &rest args)
1338+
(setq shown-message (apply #'format fmt args)))))
1339+
(pi-coding-agent-select-thinking)
1340+
(should (equal (plist-get pi-coding-agent--state :thinking-level) "low"))))
1341+
(should (equal rpc-commands
1342+
'((:type "set_thinking_level" :level "high"))))
1343+
(should (equal shown-message
1344+
"Pi: Failed to set thinking level: unsupported"))))
1345+
1346+
(ert-deftest pi-coding-agent-test-select-thinking-warns-when-state-refresh-fails ()
1347+
"Thinking selector warns instead of guessing when state refresh fails."
1348+
(let (shown-message)
1349+
(with-temp-buffer
1350+
(pi-coding-agent-chat-mode)
1351+
(setq pi-coding-agent--process :fake-proc
1352+
pi-coding-agent--state '(:thinking-level "low"))
1353+
(cl-letf (((symbol-function 'completing-read)
1354+
(lambda (&rest _) "high"))
1355+
((symbol-function 'pi-coding-agent--rpc-async)
1356+
(lambda (_proc cmd callback)
1357+
(pcase (plist-get cmd :type)
1358+
("set_thinking_level"
1359+
(funcall callback '(:success t :command "set_thinking_level")))
1360+
("get_state"
1361+
(funcall callback '(:success nil :error "state unavailable"))))))
1362+
((symbol-function 'message)
1363+
(lambda (fmt &rest args)
1364+
(setq shown-message (apply #'format fmt args)))))
1365+
(pi-coding-agent-select-thinking)
1366+
(should (equal (plist-get pi-coding-agent--state :thinking-level) "low"))))
1367+
(should (equal shown-message
1368+
"Pi: Thinking level updated, but failed to refresh state: state unavailable"))))
1369+
1370+
(ert-deftest pi-coding-agent-test-thinking-selector-uses-t-key-leaving-T-for-templates ()
1371+
"Main menu binds `t' to thinking selection without taking Templates `T'."
1372+
(let ((pi-coding-agent--commands
1373+
'((:name "review" :description "Code review" :source "prompt"))))
1374+
(unwind-protect
1375+
(progn
1376+
(pi-coding-agent--rebuild-commands-menu)
1377+
(transient-setup 'pi-coding-agent-menu)
1378+
(let ((thinking-suffix
1379+
(cl-find-if (lambda (obj)
1380+
(equal (oref obj key) "t"))
1381+
transient--suffixes))
1382+
(templates-suffix
1383+
(cl-find-if (lambda (obj)
1384+
(equal (oref obj key) "T"))
1385+
transient--suffixes)))
1386+
(should thinking-suffix)
1387+
(should (eq (oref thinking-suffix command)
1388+
'pi-coding-agent-select-thinking))
1389+
(should templates-suffix)))
1390+
(ignore-errors (transient-remove-suffix 'pi-coding-agent-menu '(4))))))
1391+
12621392
;;; sourceInfo normalization
12631393

12641394
(ert-deftest pi-coding-agent-test-normalize-command-extracts-source-info ()

0 commit comments

Comments
 (0)