From 13120440f92dc3cc9f54c94cb28eb72fc0bf9077 Mon Sep 17 00:00:00 2001 From: Daniel Nouri Date: Fri, 6 Mar 2026 23:11:44 +0100 Subject: [PATCH] fix: normalize header extension status rendering --- pi-coding-agent-ui.el | 15 ++++++++++----- test/pi-coding-agent-input-test.el | 11 +++++++++++ test/pi-coding-agent-render-test.el | 9 ++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/pi-coding-agent-ui.el b/pi-coding-agent-ui.el index 3de3243..404a135 100644 --- a/pi-coding-agent-ui.el +++ b/pi-coding-agent-ui.el @@ -1239,9 +1239,9 @@ Returns nil if CONTEXT-WINDOW is 0." (if (null context-tokens) (format " ?/%s" (pi-coding-agent--format-tokens-compact context-window)) (let* ((pct (* (/ (float context-tokens) context-window) 100)) - ;; Note: %% needed because % has special meaning in header-line-format - (pct-str (format " %.1f%%%%/%s" pct - (pi-coding-agent--format-tokens-compact context-window)))) + (pct-str (pi-coding-agent--header-escape-text + (format " %.1f%%/%s" pct + (pi-coding-agent--format-tokens-compact context-window))))) (propertize pct-str 'face (cond ((> pct pi-coding-agent-context-error-threshold) 'error) @@ -1269,13 +1269,17 @@ Returns nil if STATS is nil." (format " $%.2f" cost) (pi-coding-agent--header-format-context context-tokens context-window))))) +(defun pi-coding-agent--header-escape-text (text) + "Escape TEXT for use in `header-line-format'." + (replace-regexp-in-string "%" "%%" text t t)) + (defun pi-coding-agent--header-format-extension-status (ext-status) "Format EXT-STATUS alist for header-line display. Returns extension statuses joined with \" · \", or empty string." (if (null ext-status) "" (mapconcat (lambda (pair) - (propertize (cdr pair) 'face 'pi-coding-agent-retry-notice)) + (pi-coding-agent--header-escape-text (cdr pair))) ext-status " · "))) @@ -1310,7 +1314,8 @@ Returns a leading-pipe group string or empty string when no extension info exists." (let* ((status-str (pi-coding-agent--header-format-extension-status ext-status)) (working-str (if (and working-message (not (string-empty-p working-message))) - (propertize working-message 'face 'shadow) + (propertize (pi-coding-agent--header-escape-text working-message) + 'face 'shadow) "")) (parts nil)) (unless (string-empty-p status-str) diff --git a/test/pi-coding-agent-input-test.el b/test/pi-coding-agent-input-test.el index 8d6f6a0..208b4c7 100644 --- a/test/pi-coding-agent-input-test.el +++ b/test/pi-coding-agent-input-test.el @@ -2214,6 +2214,17 @@ Pi handles command expansion on the server side." (should (string-match-p "My Session" header)) (should (string-match-p "Git: synced · 📖 Skimming…" header))))) +(ert-deftest pi-coding-agent-test-header-extension-group-escapes-percent-signs () + "Extension header text escapes percent signs for header-line display." + (with-temp-buffer + (pi-coding-agent-chat-mode) + (setq pi-coding-agent--state '(:model (:name "gpt-5.4" :contextWindow 200000)) + pi-coding-agent--extension-status '(("sub-status:usage" . "5h 4% · Week 3% · degraded")) + pi-coding-agent--working-message "refresh 50%") + (let ((header (substring-no-properties (pi-coding-agent--header-line-string)))) + (should (string-match-p "5h 4%% · Week 3%% · degraded" header)) + (should (string-match-p "refresh 50%%" header))))) + (ert-deftest pi-coding-agent-test-header-session-name-in-context-group () "Context group shows session name when set, collapses when nil." (with-temp-buffer diff --git a/test/pi-coding-agent-render-test.el b/test/pi-coding-agent-render-test.el index 6476b5d..af75102 100644 --- a/test/pi-coding-agent-render-test.el +++ b/test/pi-coding-agent-render-test.el @@ -1182,13 +1182,16 @@ since we don't display them locally. Let pi's message_start handle it." (should (null pi-coding-agent--working-message)))) (ert-deftest pi-coding-agent-test-header-format-extension-status () - "Extension status formatter returns inline status text without pipe." + "Extension status formatter returns inline neutral status text without pipe." ;; Empty status returns empty string (should (equal (pi-coding-agent--header-format-extension-status nil) "")) ;; Single status - (let ((result (pi-coding-agent--header-format-extension-status '(("ext1" . "Processing..."))))) + (let* ((result (pi-coding-agent--header-format-extension-status '(("ext1" . "Processing...")))) + (pos (string-match "Processing" result))) (should-not (string-match-p "│" result)) - (should (string-match-p "Processing" result))) + (should (string-match-p "Processing" result)) + (should pos) + (should-not (get-text-property pos 'face result))) ;; Multiple statuses joined with separator (let ((result (pi-coding-agent--header-format-extension-status '(("ext1" . "Status 1") ("ext2" . "Status 2")))))