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
1 change: 1 addition & 0 deletions pi-coding-agent-menu.el
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ Call this when starting a new session to ensure no stale state persists."
pi-coding-agent--tool-block-order-counter 0
pi-coding-agent--thinking-block-order-counter 0
pi-coding-agent--activity-phase "idle")
(pi-coding-agent--clear-unsupported-extension-ui-warnings)
(pi-coding-agent--invalidate-history-loads)
;; Use accessors for cross-module state
(pi-coding-agent--clear-followup-queue)
Expand Down
36 changes: 28 additions & 8 deletions pi-coding-agent-render.el
Original file line number Diff line number Diff line change
Expand Up @@ -790,16 +790,36 @@ Shows success or final failure with raw error."
(setq pi-coding-agent--working-message msg)
(force-mode-line-update t)))

(defconst pi-coding-agent--extension-ui-fire-and-forget-methods
'("notify" "setStatus" "setWidget" "setTitle" "set_editor_text"
"setWorkingMessage")
"Extension UI methods that do not expect RPC responses.")

(defun pi-coding-agent--extension-ui-response-required-p (method)
"Return non-nil when unsupported extension UI METHOD may expect a response."
(not (member method pi-coding-agent--extension-ui-fire-and-forget-methods)))

(defun pi-coding-agent--extension-ui-warn-unsupported-once (method)
"Warn at most once per pi session for unsupported extension UI METHOD."
(when (pi-coding-agent--record-unsupported-extension-ui-warning method)
(message "Pi: extension UI method `%s' not supported in Emacs" method)))

(defun pi-coding-agent--extension-ui-unsupported (event proc)
"Handle unsupported method from EVENT by warning and sending cancelled via PROC.
"Handle unsupported method from EVENT, using PROC to cancel when needed.
Warn at most once per method in a pi session.
Dialog-like methods receive a cancelled response so extensions do not hang;
fire-and-forget methods are only warned because they do not expect responses.
See URL `https://github.com/dnouri/pi-coding-agent/issues/176'."
(message "Pi: extension UI method `%s' not supported in Emacs"
(plist-get event :method))
(when proc
(pi-coding-agent--send-extension-ui-response
proc (list :type "extension_ui_response"
:id (plist-get event :id)
:cancelled t))))
(let ((method (plist-get event :method))
(id (plist-get event :id)))
(pi-coding-agent--extension-ui-warn-unsupported-once method)
(when (and proc
id
(pi-coding-agent--extension-ui-response-required-p method))
(pi-coding-agent--send-extension-ui-response
proc (list :type "extension_ui_response"
:id id
:cancelled t)))))

(defun pi-coding-agent--handle-extension-ui-request (event)
"Handle extension_ui_request EVENT from pi.
Expand Down
22 changes: 21 additions & 1 deletion pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,20 @@ Keys are extension identifiers (strings), values are status text.")
(defvar-local pi-coding-agent--working-message nil
"Transient extension working message for header-line display.")

(defvar-local pi-coding-agent--unsupported-extension-ui-methods-warned nil
"Unsupported extension UI method names already warned for this pi session.")

(defun pi-coding-agent--record-unsupported-extension-ui-warning (method)
"Record an unsupported extension UI warning for METHOD.
Return non-nil when METHOD had not already been warned for this pi session."
(unless (member method pi-coding-agent--unsupported-extension-ui-methods-warned)
(push method pi-coding-agent--unsupported-extension-ui-methods-warned)
t))

(defun pi-coding-agent--clear-unsupported-extension-ui-warnings ()
"Forget unsupported extension UI warnings for the current pi session."
(setq pi-coding-agent--unsupported-extension-ui-methods-warned nil))

(defvar-local pi-coding-agent--session-name nil
"Cached session name for header-line display.
Extracted from session_info entries when session is loaded or switched.")
Expand Down Expand Up @@ -1742,7 +1756,13 @@ Safely handles dead buffers by checking liveness first."
(when (and (eq (plist-get response :success) t)
(buffer-live-p chat-buf))
(with-current-buffer chat-buf
(let ((new-state (pi-coding-agent--extract-state-from-response response)))
(let* ((old-session-id (plist-get pi-coding-agent--state :session-id))
(new-state (pi-coding-agent--extract-state-from-response response))
(new-session-id (plist-get new-state :session-id)))
(when (and old-session-id
new-session-id
(not (equal old-session-id new-session-id)))
(pi-coding-agent--clear-unsupported-extension-ui-warnings))
(setq pi-coding-agent--status (plist-get new-state :status)
pi-coding-agent--state new-state))
(force-mode-line-update t))))
Expand Down
2 changes: 2 additions & 0 deletions test/pi-coding-agent-menu-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
pi-coding-agent--aborted t
pi-coding-agent--extension-status '(("ext1" . "status"))
pi-coding-agent--working-message "Reading README..."
pi-coding-agent--unsupported-extension-ui-methods-warned '("setWidget")
pi-coding-agent--message-start-marker (point-marker)
pi-coding-agent--streaming-marker (point-marker)
pi-coding-agent--thinking-marker (point-marker)
Expand All @@ -102,6 +103,7 @@
(should (null pi-coding-agent--aborted))
(should (null pi-coding-agent--extension-status))
(should (null pi-coding-agent--working-message))
(should (null pi-coding-agent--unsupported-extension-ui-methods-warned))
(should (null pi-coding-agent--message-start-marker))
(should (null pi-coding-agent--streaming-marker))
(should (null pi-coding-agent--thinking-marker))
Expand Down
95 changes: 86 additions & 9 deletions test/pi-coding-agent-render-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1936,9 +1936,13 @@ since we don't display them locally. Let pi's message_start handle it."
(ert-deftest pi-coding-agent-test-extension-ui-unsupported-warns ()
"Unsupported extension_ui_request method warns via `message'.
See https://github.com/dnouri/pi-coding-agent/issues/176."
(let (messages-logged response-sent)
(let (warnings-logged response-sent)
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args) (push (apply #'format fmt args) messages-logged)))
(lambda (fmt &rest args)
(when fmt
(let ((msg (apply #'format fmt args)))
(when (string-match-p "extension UI method" msg)
(push msg warnings-logged))))))
((symbol-function 'pi-coding-agent--send-extension-ui-response)
(lambda (_proc resp) (setq response-sent resp))))
(with-temp-buffer
Expand All @@ -1948,12 +1952,87 @@ See https://github.com/dnouri/pi-coding-agent/issues/176."
'(:type "extension_ui_request"
:id "req-unknown"
:method "someNewFancyWidget")))))
;; Should warn the user
(should (cl-some (lambda (m) (string-match-p "someNewFancyWidget" m))
messages-logged))
;; Should still send cancelled so the extension doesn't hang
warnings-logged))
;; Unknown methods may be future dialogs, so they are cancelled.
(should response-sent)
(should (equal (plist-get response-sent :cancelled) t))))
(should (eq (plist-get response-sent :cancelled) t))))

(ert-deftest pi-coding-agent-test-extension-ui-unsupported-warns-once-per-method ()
"Repeated unsupported extension_ui_request methods warn once per method."
(let (warnings-logged responses-sent)
(with-temp-buffer
(pi-coding-agent-chat-mode)
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(when fmt
(let ((msg (apply #'format fmt args)))
(when (string-match-p "extension UI method" msg)
(push msg warnings-logged))))))
((symbol-function 'pi-coding-agent--send-extension-ui-response)
(lambda (_proc resp) (push resp responses-sent))))
(let ((pi-coding-agent--process t))
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-widget-1"
:method "setWidget"
:widgetKey "my-ext"
:widgetLines ["Line 1"]))
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-widget-2"
:method "setWidget"
:widgetKey "my-ext"
:widgetLines ["Line 2"]))
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-title"
:method "setTitle"
:title "pi - project")))))
(should (= (length warnings-logged) 2))
(should (= 1 (cl-count-if (lambda (m) (string-match-p "setWidget" m))
warnings-logged)))
(should (= 1 (cl-count-if (lambda (m) (string-match-p "setTitle" m))
warnings-logged)))
;; setWidget and setTitle are fire-and-forget RPC methods.
(should (null responses-sent))))

(ert-deftest pi-coding-agent-test-extension-ui-unsupported-warnings-are-buffer-local ()
"Unsupported extension UI warning dedupe is isolated by chat buffer."
(let ((buf-a (generate-new-buffer "*test-extension-ui-a*"))
(buf-b (generate-new-buffer "*test-extension-ui-b*"))
warnings-logged)
(unwind-protect
(cl-letf (((symbol-function 'message)
(lambda (fmt &rest args)
(when fmt
(let ((msg (apply #'format fmt args)))
(when (string-match-p "extension UI method" msg)
(push msg warnings-logged))))))
((symbol-function 'pi-coding-agent--send-extension-ui-response)
#'ignore))
(dolist (buf (list buf-a buf-b))
(with-current-buffer buf
(pi-coding-agent-chat-mode)
(let ((pi-coding-agent--process t))
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-widget"
:method "setWidget"
:widgetKey "my-ext"
:widgetLines ["Line 1"])))))
(with-current-buffer buf-a
(let ((pi-coding-agent--process t))
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-widget-again"
:method "setWidget"
:widgetKey "my-ext"
:widgetLines ["Line 2"]))))
(should (= 2 (cl-count-if (lambda (m) (string-match-p "setWidget" m))
warnings-logged))))
(when (buffer-live-p buf-a) (kill-buffer buf-a))
(when (buffer-live-p buf-b) (kill-buffer buf-b)))))

(ert-deftest pi-coding-agent-test-header-format-extension-status ()
"Extension status formatter returns inline neutral status text without pipe."
Expand Down Expand Up @@ -1987,9 +2066,7 @@ See https://github.com/dnouri/pi-coding-agent/issues/176."
(pi-coding-agent--handle-extension-ui-request
'(:type "extension_ui_request"
:id "req-9"
:method "setWidget"
:widgetKey "my-ext"
:widgetLines ["Line 1"])))
:method "someNewFancyWidget")))
(should response-sent)
(should (equal (plist-get response-sent :type) "extension_ui_response"))
(should (equal (plist-get response-sent :id) "req-9"))
Expand Down
54 changes: 54 additions & 0 deletions test/pi-coding-agent-ui-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,60 @@ Catches wiring bugs like requiring deleted modules."
(should essential-called)
(should optional-called))))

;;; State response

(ert-deftest pi-coding-agent-test-apply-state-response-preserves-extension-ui-warnings-without-session-change ()
"Applying state keeps unsupported UI warnings within the same pi session."
(let ((chat-buf (generate-new-buffer "*test-state-same-session*")))
(unwind-protect
(progn
(with-current-buffer chat-buf
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--state nil
pi-coding-agent--unsupported-extension-ui-methods-warned
'("setWidget")))
(pi-coding-agent--apply-state-response
chat-buf
'(:success t :data (:isStreaming :false
:sessionId "new-session"
:sessionFile "/tmp/new.jsonl")))
(with-current-buffer chat-buf
(should (equal pi-coding-agent--unsupported-extension-ui-methods-warned
'("setWidget"))))
(with-current-buffer chat-buf
(setq pi-coding-agent--unsupported-extension-ui-methods-warned
'("setWidget")))
(pi-coding-agent--apply-state-response
chat-buf
'(:success t :data (:isStreaming :false
:sessionId "new-session"
:sessionFile "/tmp/newer.jsonl")))
(with-current-buffer chat-buf
(should (equal pi-coding-agent--unsupported-extension-ui-methods-warned
'("setWidget")))))
(kill-buffer chat-buf))))

(ert-deftest pi-coding-agent-test-apply-state-response-resets-extension-ui-warnings-on-session-change ()
"Applying state clears unsupported UI warnings when the pi session changes."
(let ((chat-buf (generate-new-buffer "*test-state-session-change*")))
(unwind-protect
(progn
(with-current-buffer chat-buf
(pi-coding-agent-chat-mode)
(setq pi-coding-agent--state '(:session-id "old-session")
pi-coding-agent--unsupported-extension-ui-methods-warned
'("setWidget")))
(pi-coding-agent--apply-state-response
chat-buf
'(:success t :data (:isStreaming :false
:sessionId "new-session"
:sessionFile "/tmp/new.jsonl")))
(with-current-buffer chat-buf
(should (equal (plist-get pi-coding-agent--state :session-id)
"new-session"))
(should (null pi-coding-agent--unsupported-extension-ui-methods-warned))))
(kill-buffer chat-buf))))

;;; Input Window Height (integer and float ratio)

(ert-deftest pi-coding-agent-test-input-height-integer-returns-configured-value ()
Expand Down
Loading