Integrated repository, worktree, git stacker, and project manager
0

Configure Feed

Select the types of activity you want to include in your feed.

emacs: RET on a sidebar project heading opens the project dispatch

pm-sidebar-visit now branches on the section: a session row still jumps to
its terminal buffer; a project heading opens `pm-project-dispatch' scoped to
that project (binds default-directory to its container so the dispatch path
resolves without prompting). The "(no project)" placeholder errors. Adds
red/green tests for both project branches.

Jordan Isaacs (Jun 22, 2026, 8:42 PM UTC) 987ea0a2 a139a5c3

+73 -10
+29 -10
integrations/emacs/pm-sidebar.el
··· 23 23 24 24 (require 'pm-faces) 25 25 (require 'pm-table) ; pm-propertize-face + section reset/cover/show helpers 26 - (require 'pm-agent) ; pm-agent-serve-source, pm-agent-buffer-for-id 26 + (require 'pm-agent) ; pm-agent-serve-source, pm-agent-buffer-for-id, pm-agent--cwd 27 + 28 + ;; Autoloaded from pm-transient; called by RET on a project heading. 29 + (declare-function pm-project-dispatch "pm-transient" (&optional arg)) 27 30 28 31 (defgroup pm-sidebar nil 29 32 "Live agent-session sidebar fed by `pm agent serve'." ··· 40 43 :group 'pm-sidebar) 41 44 42 45 (defconst pm-sidebar-buffer-name "*pm-agents*") 46 + 47 + ;; Heading shown for sessions with no resolved pm project. Kept as a constant 48 + ;; so the render and the RET handler agree on what is *not* a real project. 49 + (defconst pm-sidebar--no-project "(no project)") 43 50 44 51 ;; --- faces ------------------------------------------------------------------ 45 52 (defface pm-sidebar-working '((t :inherit success :weight bold)) "Agent is working.") ··· 173 180 (insert "\n" (pm-propertize-face "no active sessions" 'pm-sidebar-idle) "\n") 174 181 (let ((groups (make-hash-table :test 'equal)) (order '())) 175 182 (maphash (lambda (_k s) 176 - (let ((p (or (alist-get 'project s) "(no project)"))) 183 + (let ((p (or (alist-get 'project s) pm-sidebar--no-project))) 177 184 (unless (gethash p groups) (push p order)) 178 185 (push s (gethash p groups)))) 179 186 pm-sidebar--sessions) ··· 197 204 198 205 ;; --- actions ---------------------------------------------------------------- 199 206 (defun pm-sidebar-visit () 200 - "Jump to the terminal buffer of the agent session at point. 201 - Uses the session's `PM_META_BUF' (the launching buffer's builtin id)." 207 + "Act on the thing at point. 208 + On a session row, jump to its terminal buffer (via the session's 209 + `PM_META_BUF'). On a project heading, open `pm-project-dispatch' scoped to 210 + that project." 202 211 (interactive) 203 212 (let* ((sec (magit-current-section)) 204 - (s (and sec (eq (oref sec type) 'pm-sidebar-session) (oref sec value))) 205 - (bufid (and s (alist-get 'BUF (alist-get 'meta s)))) 206 - (buf (pm-agent-buffer-for-id bufid))) 207 - (cond (buf (pop-to-buffer buf)) 208 - (s (user-error "No live buffer for this session (not launched here, or closed)")) 209 - (t (user-error "Point is not on a session"))))) 213 + (type (and sec (oref sec type))) 214 + (val (and sec (oref sec value)))) 215 + (cond 216 + ((eq type 'pm-sidebar-session) 217 + (let ((buf (pm-agent-buffer-for-id (alist-get 'BUF (alist-get 'meta val))))) 218 + (if buf (pop-to-buffer buf) 219 + (user-error "No live buffer for this session (not launched here, or closed)")))) 220 + ((eq type 'pm-sidebar-project) 221 + (when (equal val pm-sidebar--no-project) 222 + (user-error "Not a pm project")) 223 + ;; Scope the dispatch to this project's container so 224 + ;; `pm--resolve-dispatch-path' (which keys off `default-directory') picks 225 + ;; it without prompting. 226 + (let ((default-directory (pm-agent--cwd val))) 227 + (pm-project-dispatch))) 228 + (t (user-error "Point is not on a session or project"))))) 210 229 211 230 ;; --- SSE plumbing ----------------------------------------------------------- 212 231 (defun pm-sidebar--filter-query ()
+44
integrations/emacs/pm-tests.el
··· 1138 1138 (should (= (point) saved))))) 1139 1139 (when (get-buffer pm-sidebar-buffer-name) 1140 1140 (let ((kill-buffer-query-functions nil)) 1141 + (kill-buffer pm-sidebar-buffer-name)))))) 1142 + 1143 + (ert-deftest pm-test--sidebar-visit-project-opens-dispatch () 1144 + "RET on a project heading opens `pm-project-dispatch' scoped to it." 1145 + (let ((pm-sidebar--sessions (make-hash-table :test 'equal)) 1146 + (pm-projects-dir "/tmp/pm-test-projects/") 1147 + (captured nil)) 1148 + (puthash "claude\0s1" 1149 + '((agent . "claude") (vendor_session_id . "s1") (project . "alpha") 1150 + (title . "A") (status . "idle")) 1151 + pm-sidebar--sessions) 1152 + (unwind-protect 1153 + (cl-letf (((symbol-function 'pm-project-dispatch) 1154 + (lambda (&rest _) (setq captured default-directory)))) 1155 + (pm-sidebar--render) 1156 + (with-current-buffer pm-sidebar-buffer-name 1157 + (goto-char (point-min)) 1158 + (should (search-forward "alpha" nil t)) 1159 + (beginning-of-line) 1160 + (pm-sidebar-visit) 1161 + (should (string= captured 1162 + (file-name-as-directory 1163 + (expand-file-name "alpha" "/tmp/pm-test-projects/")))))) 1164 + (when (get-buffer pm-sidebar-buffer-name) 1165 + (let ((kill-buffer-query-functions nil)) 1166 + (kill-buffer pm-sidebar-buffer-name)))))) 1167 + 1168 + (ert-deftest pm-test--sidebar-visit-no-project-errors () 1169 + "RET on the placeholder \"(no project)\" heading errors, never dispatches." 1170 + (let ((pm-sidebar--sessions (make-hash-table :test 'equal))) 1171 + (puthash "claude\0s1" 1172 + '((agent . "claude") (vendor_session_id . "s1") 1173 + (title . "A") (status . "idle")) 1174 + pm-sidebar--sessions) 1175 + (unwind-protect 1176 + (progn 1177 + (pm-sidebar--render) 1178 + (with-current-buffer pm-sidebar-buffer-name 1179 + (goto-char (point-min)) 1180 + (should (search-forward "(no project)" nil t)) 1181 + (beginning-of-line) 1182 + (should-error (pm-sidebar-visit) :type 'user-error))) 1183 + (when (get-buffer pm-sidebar-buffer-name) 1184 + (let ((kill-buffer-query-functions nil)) 1141 1185 (kill-buffer pm-sidebar-buffer-name))))))) 1142 1186 1143 1187 (provide 'pm-tests)