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
27 changes: 27 additions & 0 deletions pi-coding-agent-input.el
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
;; `pi-coding-agent-quit' Close session
;; `pi-coding-agent-previous-input' History backward (M-p)
;; `pi-coding-agent-next-input' History forward (M-n)
;; `pi-coding-agent-input-previous-message' Navigate previous chat message
;; `pi-coding-agent-input-next-message' Navigate next chat message
;; `pi-coding-agent-history-isearch-backward' History search (C-r)
;; `pi-coding-agent-queue-steering' Steering message (C-c C-s)

Expand Down Expand Up @@ -252,6 +254,31 @@ markdown highlighting while preserving mode identity and keybindings."
(add-hook 'isearch-mode-hook #'pi-coding-agent--history-isearch-setup nil t)
(add-hook 'kill-buffer-hook #'pi-coding-agent--cleanup-input-on-kill nil t))

;;;; Input-Buffer Chat Navigation

(defun pi-coding-agent--call-in-visible-chat-window (fn)
"Call FN in the visible linked chat window, preserving input focus."
(let* ((chat-buf (pi-coding-agent--get-chat-buffer))
(win (and (buffer-live-p chat-buf)
(get-buffer-window chat-buf))))
(if (window-live-p win)
(save-selected-window
(select-window win)
(funcall fn))
(user-error "No chat window visible"))))

(defun pi-coding-agent-input-next-message ()
"Move chat to the next user message, keeping focus in input."
(interactive)
(pi-coding-agent--call-in-visible-chat-window
#'pi-coding-agent-next-message))

(defun pi-coding-agent-input-previous-message ()
"Move chat to the previous user message, keeping focus in input."
(interactive)
(pi-coding-agent--call-in-visible-chat-window
#'pi-coding-agent-previous-message))

;;;; Sending Prompts

(defun pi-coding-agent--accept-input-text (text)
Expand Down
9 changes: 6 additions & 3 deletions pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,6 @@ Returns the position of the heading line start, or nil if not found."
(when (get-buffer-window) (recenter 0)))
(message "No previous message"))))


;;;; Copy Visible Text

(defun pi-coding-agent--visible-text (beg end)
Expand Down Expand Up @@ -623,8 +622,12 @@ new live processes in interactive sessions."
"Reference to the chat buffer for this session.")

(defun pi-coding-agent--set-chat-buffer (buffer)
"Set the chat BUFFER reference for this session."
(setq pi-coding-agent--chat-buffer buffer))
"Set the chat BUFFER reference for this session.
In input buffers, also store BUFFER in `other-window-scroll-buffer'
so built-in other-window scrolling commands target the linked chat."
(setq pi-coding-agent--chat-buffer buffer)
(when (derived-mode-p 'pi-coding-agent-input-mode)
(setq-local other-window-scroll-buffer buffer)))

(defvar-local pi-coding-agent--input-buffer nil
"Reference to the input buffer for this session.")
Expand Down
83 changes: 83 additions & 0 deletions test/pi-coding-agent-input-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -2915,5 +2915,88 @@ no spurious faces are applied to plain colon-ending lines."
(should (eq (key-binding (kbd "TAB")) 'pi-coding-agent-complete))
(should (eq (key-binding (kbd "C-c C-s")) 'pi-coding-agent-queue-steering))))

;;; Input-Buffer Chat Navigation

(ert-deftest pi-coding-agent-test-input-next-message-moves-chat ()
"Input-side next-message moves the linked chat and keeps focus."
(let ((chat-buf (generate-new-buffer "*test-chat*"))
(input-buf (generate-new-buffer "*test-input*")))
(unwind-protect
(progn
(delete-other-windows)
(switch-to-buffer chat-buf)
(with-current-buffer chat-buf
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(pi-coding-agent-test--insert-chat-turns))
(pi-coding-agent--set-input-buffer input-buf)
(goto-char (point-min)))
(let ((input-win (split-window nil -10 'below)))
(set-window-buffer input-win input-buf)
(with-current-buffer input-buf
(pi-coding-agent-input-mode)
(pi-coding-agent--set-chat-buffer chat-buf))
(select-window input-win)
(pi-coding-agent-input-next-message)
(with-current-buffer chat-buf
(should (looking-at "You · 10:00")))
(should (eq (window-buffer (selected-window)) input-buf))))
(kill-buffer chat-buf)
(kill-buffer input-buf)
(delete-other-windows))))

(ert-deftest pi-coding-agent-test-input-previous-message-moves-linked-chat ()
"Input-side previous-message uses the linked chat, not scroll state."
(let ((chat-a (generate-new-buffer "*test-chat-a*"))
(chat-b (generate-new-buffer "*test-chat-b*"))
(input-buf (generate-new-buffer "*test-input*")))
(unwind-protect
(progn
(delete-other-windows)
(switch-to-buffer chat-a)
(let* ((chat-win-a (selected-window))
(input-win (split-window chat-win-a -10 'below))
(chat-win-b (split-window chat-win-a nil 'right)))
(set-window-buffer input-win input-buf)
(set-window-buffer chat-win-b chat-b)
(with-current-buffer chat-a
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(pi-coding-agent-test--insert-chat-turns))
(goto-char (point-max))
(re-search-backward "^You · 10:10$" nil t))
(with-current-buffer chat-b
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(pi-coding-agent-test--insert-chat-turns))
(goto-char (point-max))
(re-search-backward "^You · 10:10$" nil t))
(with-current-buffer input-buf
(pi-coding-agent-input-mode)
(pi-coding-agent--set-chat-buffer chat-a)
(setq-local other-window-scroll-buffer chat-b))
(select-window input-win)
(pi-coding-agent-input-previous-message)
(with-current-buffer chat-a
(should (looking-at "You · 10:05")))
(with-current-buffer chat-b
(should (looking-at "You · 10:10")))
(should (eq (window-buffer (selected-window)) input-buf))))
(kill-buffer chat-a)
(kill-buffer chat-b)
(kill-buffer input-buf)
(delete-other-windows))))

(ert-deftest pi-coding-agent-test-input-previous-message-no-chat-window-errors ()
"Navigating from input without a visible linked chat signals error."
(let ((chat-buf (generate-new-buffer "*test-chat-hidden*")))
(unwind-protect
(with-temp-buffer
(pi-coding-agent-input-mode)
(pi-coding-agent--set-chat-buffer chat-buf)
(should-error (pi-coding-agent-input-previous-message)
:type 'user-error))
(kill-buffer chat-buf))))

(provide 'pi-coding-agent-input-test)
;;; pi-coding-agent-input-test.el ends here
60 changes: 60 additions & 0 deletions test/pi-coding-agent-ui-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,66 @@ Buffer is read-only with `inhibit-read-only' used for insertion.
(should (= (point) pos))
(should (equal shown-message "No previous message")))))

(ert-deftest pi-coding-agent-test-other-window-scroll-buffer-set-locally ()
"Session setup stores `other-window-scroll-buffer' as input-local state."
(let ((root "/tmp/pi-coding-agent-test-scroll-other/")
(original-default (default-value 'other-window-scroll-buffer)))
(make-directory root t)
(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))
(unwind-protect
(progn
(let ((default-directory root))
(pi-coding-agent))
(let ((chat (get-buffer (pi-coding-agent-test--chat-buffer-name root)))
(input (get-buffer (pi-coding-agent-test--input-buffer-name root))))
(with-current-buffer input
(should (local-variable-p 'other-window-scroll-buffer))
(should (eq other-window-scroll-buffer chat)))
(should (eq (default-value 'other-window-scroll-buffer)
original-default))))
(set-default 'other-window-scroll-buffer original-default)
(pi-coding-agent-test--kill-session-buffers root)))))

(ert-deftest pi-coding-agent-test-other-window-for-scrolling-tracks-each-input-session ()
"Each input buffer scrolls its own chat buffer."
(let ((root-a "/tmp/pi-coding-agent-test-scroll-a/")
(root-b "/tmp/pi-coding-agent-test-scroll-b/")
(original-default (default-value 'other-window-scroll-buffer)))
(make-directory root-a t)
(make-directory root-b t)
(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))
(unwind-protect
(progn
(let ((default-directory root-a))
(pi-coding-agent))
(let ((default-directory root-b))
(pi-coding-agent))
(let* ((chat-a (get-buffer (pi-coding-agent-test--chat-buffer-name root-a)))
(input-a (get-buffer (pi-coding-agent-test--input-buffer-name root-a)))
(chat-b (get-buffer (pi-coding-agent-test--chat-buffer-name root-b)))
(input-b (get-buffer (pi-coding-agent-test--input-buffer-name root-b))))
(delete-other-windows)
(switch-to-buffer chat-a)
(let* ((chat-win-a (selected-window))
(input-win-a (split-window chat-win-a -10 'below))
(chat-win-b (split-window chat-win-a nil 'right))
input-win-b)
(set-window-buffer input-win-a input-a)
(set-window-buffer chat-win-b chat-b)
(setq input-win-b (split-window chat-win-b -10 'below))
(set-window-buffer input-win-b input-b)
(select-window input-win-a)
(should (eq (window-buffer (other-window-for-scrolling)) chat-a))
(select-window input-win-b)
(should (eq (window-buffer (other-window-for-scrolling)) chat-b)))))
(set-default 'other-window-scroll-buffer original-default)
(pi-coding-agent-test--kill-session-buffers root-a)
(pi-coding-agent-test--kill-session-buffers root-b)))))

;;; Turn Detection

(ert-deftest pi-coding-agent-test-turn-index-on-first-heading ()
Expand Down
Loading