diff --git a/pi-coding-agent-menu.el b/pi-coding-agent-menu.el index 3ce9d26..c705b08 100644 --- a/pi-coding-agent-menu.el +++ b/pi-coding-agent-menu.el @@ -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) diff --git a/pi-coding-agent-render.el b/pi-coding-agent-render.el index 1665859..9fbc4f8 100644 --- a/pi-coding-agent-render.el +++ b/pi-coding-agent-render.el @@ -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. diff --git a/pi-coding-agent-ui.el b/pi-coding-agent-ui.el index 87c8898..0f93dd9 100644 --- a/pi-coding-agent-ui.el +++ b/pi-coding-agent-ui.el @@ -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.") @@ -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)))) diff --git a/test/pi-coding-agent-menu-test.el b/test/pi-coding-agent-menu-test.el index c06988a..d14cbf0 100644 --- a/test/pi-coding-agent-menu-test.el +++ b/test/pi-coding-agent-menu-test.el @@ -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) @@ -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)) diff --git a/test/pi-coding-agent-render-test.el b/test/pi-coding-agent-render-test.el index 93a2a2b..461cd36 100644 --- a/test/pi-coding-agent-render-test.el +++ b/test/pi-coding-agent-render-test.el @@ -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 @@ -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." @@ -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")) diff --git a/test/pi-coding-agent-ui-test.el b/test/pi-coding-agent-ui-test.el index 11c4827..28d0a08 100644 --- a/test/pi-coding-agent-ui-test.el +++ b/test/pi-coding-agent-ui-test.el @@ -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 ()