Skip to content
Open
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
39 changes: 39 additions & 0 deletions pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,45 @@ TYPE is :chat or :input. Returns the buffer."
(:input (pi-coding-agent-input-mode))))
buf))))

;;;; Project Buffer Discovery

(defun pi-coding-agent-project-buffers ()
"Return all pi chat buffers for the current project directory.
Matches buffer names by prefix against the abbreviated project dir.
Returns a list ordered by `buffer-list' recency (most recent first)."
(let ((prefix (format "*pi-coding-agent-chat:%s"
(abbreviate-file-name
(pi-coding-agent--session-directory)))))
(cl-remove-if-not
(lambda (buf)
(string-prefix-p prefix (buffer-name buf)))
(buffer-list))))

;;;; Window Hiding

(defun pi-coding-agent--hide-buffers ()
"Hide all pi windows (chat and input) for the current session.
Uses `delete-window' when the frame has other windows, or
`bury-buffer' for sole-window frames. A second pass buries any
pi buffer that `switch-to-prev-buffer' may have landed on."
(let* ((chat-buf (pi-coding-agent--get-chat-buffer))
(input-buf (pi-coding-agent--get-input-buffer))
(pi-bufs (list chat-buf input-buf)))
;; First pass: delete or bury each pi window
(dolist (buf pi-bufs)
(when (buffer-live-p buf)
(dolist (win (get-buffer-window-list buf nil t))
(if (> (length (window-list (window-frame win))) 1)
(delete-window win)
(with-selected-window win
(bury-buffer))))))
;; Second pass: if bury-buffer landed on the paired pi buffer, bury again
(dolist (buf pi-bufs)
(when (buffer-live-p buf)
(dolist (win (get-buffer-window-list buf nil t))
(with-selected-window win
(bury-buffer)))))))

;;;; Buffer-Local Session Variables

(defvar-local pi-coding-agent--process nil
Expand Down
60 changes: 45 additions & 15 deletions pi-coding-agent.el
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
;; Install from: https://github.com/misohena/phscroll
;;
;; Usage:
;; M-x pi Start a session in current project
;; M-x pi DWIM: toggle, reuse, or create session
;; C-u M-x pi Start a named session
;;
;; Key Bindings:
Expand Down Expand Up @@ -129,24 +129,54 @@ Returns the chat buffer."
;;;###autoload
(defun pi-coding-agent (&optional session)
"Start or switch to pi coding agent session in current project.
With prefix arg, prompt for SESSION name to allow multiple sessions.
If already in a pi buffer and no SESSION specified, redisplays current session."
Without prefix arg, DWIM behavior:
- From a pi buffer: toggle visibility (hide windows).
- Existing session for project: switch to it.
- No session: create a new one.
With prefix arg, prompt for SESSION name to create a new session.
When called from Lisp with a string SESSION, create or switch to
that named session (backward-compatible)."
(interactive
(list (when current-prefix-arg
(read-string "Session name: "))))
(pi-coding-agent--check-dependencies)
(let (chat-buf input-buf)
(if (and (derived-mode-p 'pi-coding-agent-chat-mode 'pi-coding-agent-input-mode)
(not session))
;; Already in pi buffer with no new session requested - use current session
(setq chat-buf (pi-coding-agent--get-chat-buffer)
input-buf (pi-coding-agent--get-input-buffer))
;; Find or create session for current directory
(let ((dir (pi-coding-agent--session-directory)))
(setq chat-buf (pi-coding-agent--setup-session dir session))
(setq input-buf (buffer-local-value 'pi-coding-agent--input-buffer chat-buf))))
;; Display and focus
(pi-coding-agent--display-buffers chat-buf input-buf)))
(cond
;; In a pi buffer with no session arg: toggle visibility
((and (derived-mode-p 'pi-coding-agent-chat-mode 'pi-coding-agent-input-mode)
(not session))
(pi-coding-agent--hide-buffers))
;; Existing session for this project (no session arg): reuse it
((and (not session)
(car (pi-coding-agent-project-buffers)))
(let* ((chat-buf (car (pi-coding-agent-project-buffers)))
(input-buf (buffer-local-value 'pi-coding-agent--input-buffer chat-buf)))
(pi-coding-agent--display-buffers chat-buf input-buf)))
;; Otherwise: create or find named session
(t
(let* ((dir (pi-coding-agent--session-directory))
(chat-buf (pi-coding-agent--setup-session dir session))
(input-buf (buffer-local-value 'pi-coding-agent--input-buffer chat-buf)))
(pi-coding-agent--display-buffers chat-buf input-buf)))))

;;;###autoload
(defun pi-coding-agent-toggle ()
"Toggle pi coding agent window visibility for the current project.
If pi windows are visible, hide them. If hidden but a session
exists, show them. If no session exists, signal an error."
(interactive)
(let ((chat-buf (car (pi-coding-agent-project-buffers))))
(cond
;; No session at all
((null chat-buf)
(user-error "No pi session for this project"))
;; Session visible: hide it
((get-buffer-window-list chat-buf nil t)
(with-current-buffer chat-buf
(pi-coding-agent--hide-buffers)))
;; Session hidden: show it
(t
(let ((input-buf (buffer-local-value 'pi-coding-agent--input-buffer chat-buf)))
(pi-coding-agent--display-buffers chat-buf input-buf))))))

;;;; Performance Optimizations

Expand Down
4 changes: 2 additions & 2 deletions test/pi-coding-agent-test-common.el
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ Automatically cleans up chat and input buffers."
((symbol-function 'pi-coding-agent--display-buffers) #'ignore))
(unwind-protect
(progn (pi-coding-agent) ,@body)
(ignore-errors (kill-buffer (pi-coding-agent--chat-buffer-name ,dir nil)))
(ignore-errors (kill-buffer (pi-coding-agent--input-buffer-name ,dir nil)))))))
(ignore-errors (kill-buffer (pi-coding-agent--buffer-name :chat ,dir nil)))
(ignore-errors (kill-buffer (pi-coding-agent--buffer-name :input ,dir nil)))))))

;;;; Two-Session Fixture

Expand Down
57 changes: 57 additions & 0 deletions test/pi-coding-agent-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,62 @@
(with-current-buffer "*pi-coding-agent-input:/tmp/pi-coding-agent-test-modes/*"
(should (derived-mode-p 'pi-coding-agent-input-mode)))))

;;; DWIM & Toggle

(ert-deftest pi-coding-agent-test-dwim-reuses-existing-session ()
"Calling `pi-coding-agent' from a non-pi buffer reuses the existing session."
(pi-coding-agent-test-with-mock-session "/tmp/pi-coding-agent-test-dwim/"
;; Session exists; now call from a non-pi buffer in the same project
(with-temp-buffer
(setq default-directory "/tmp/pi-coding-agent-test-dwim/")
(cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil))
((symbol-function 'pi-coding-agent--display-buffers) #'ignore))
(pi-coding-agent))
;; Should not have created a second chat buffer
(should (= 1 (length (cl-remove-if-not
(lambda (b)
(string-prefix-p "*pi-coding-agent-chat:/tmp/pi-coding-agent-test-dwim/"
(buffer-name b)))
(buffer-list))))))))

(ert-deftest pi-coding-agent-test-new-session-with-prefix-arg ()
"\\[universal-argument] \\[pi-coding-agent] creates a named session."
(pi-coding-agent-test-with-mock-session "/tmp/pi-coding-agent-test-named/"
(cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil))
((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil))
((symbol-function 'pi-coding-agent--display-buffers) #'ignore)
((symbol-function 'read-string) (lambda (&rest _) "my-session")))
(let ((default-directory "/tmp/pi-coding-agent-test-named/")
(current-prefix-arg '(4)))
(call-interactively #'pi-coding-agent)))
(unwind-protect
(should (get-buffer "*pi-coding-agent-chat:/tmp/pi-coding-agent-test-named/<my-session>*"))
(ignore-errors
(kill-buffer "*pi-coding-agent-chat:/tmp/pi-coding-agent-test-named/<my-session>*"))
(ignore-errors
(kill-buffer "*pi-coding-agent-input:/tmp/pi-coding-agent-test-named/<my-session>*")))))

(ert-deftest pi-coding-agent-test-project-buffers-finds-session ()
"`pi-coding-agent-project-buffers' returns chat buffer for the current project."
(pi-coding-agent-test-with-mock-session "/tmp/pi-coding-agent-test-projbuf/"
(let ((default-directory "/tmp/pi-coding-agent-test-projbuf/"))
(cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)))
(should (= 1 (length (pi-coding-agent-project-buffers))))
(should (string-prefix-p "*pi-coding-agent-chat:"
(buffer-name (car (pi-coding-agent-project-buffers)))))))))

(ert-deftest pi-coding-agent-test-project-buffers-excludes-other-projects ()
"`pi-coding-agent-project-buffers' returns nil for a different project."
(pi-coding-agent-test-with-mock-session "/tmp/pi-coding-agent-test-projbuf-a/"
(let ((default-directory "/tmp/pi-coding-agent-test-projbuf-b/"))
(cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)))
(should (null (pi-coding-agent-project-buffers)))))))

(ert-deftest pi-coding-agent-test-toggle-no-session-errors ()
"`pi-coding-agent-toggle' signals `user-error' when no session exists."
(let ((default-directory "/tmp/pi-coding-agent-test-no-session/"))
(cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)))
(should-error (pi-coding-agent-toggle) :type 'user-error))))

(provide 'pi-coding-agent-test)
;;; pi-coding-agent-test.el ends here