From 91086c630e54446ec25a4a61039c8c63c432950d Mon Sep 17 00:00:00 2001 From: Sayre Blades Date: Sun, 19 Jul 2026 12:17:22 -0400 Subject: [PATCH] Add on-demand and hidden input window display modes New pi-coding-agent-input-window-display user option with three values: - `always' (the default): the input window is shown whenever the session is displayed (unchanged behavior). - `on-demand': the input window is shown at launch, hidden after each send, and reopened with the new pi-coding-agent-open-input command. Redisplaying an existing session (e.g. via pi-coding-agent-toggle) shows only the chat window. - `hidden': like `on-demand', but a session also launches with only the chat window visible, so you start in a chat-centric view and open the input only when composing. Also fix pi-coding-agent--best-input-window returning a non-input window when no input window was visible. --- README.org | 9 +++ pi-coding-agent-input.el | 2 + pi-coding-agent-ui.el | 125 ++++++++++++++++++++++++----- pi-coding-agent.el | 14 +++- test/pi-coding-agent-test.el | 14 ++-- test/pi-coding-agent-ui-test.el | 138 ++++++++++++++++++++++++++++++++ 6 files changed, 270 insertions(+), 32 deletions(-) diff --git a/README.org b/README.org index 556f826..660b954 100644 --- a/README.org +++ b/README.org @@ -438,6 +438,15 @@ Here's some common non-default preferences: ;; Make the input pane 25% of the Pi window pair; rebalanced on frame resize: (setopt pi-coding-agent-input-window-height 0.25) +;; Show the input pane only when launching a session and while composing; +;; hide it after each send and reopen it with M-x pi-coding-agent-open-input: +(setopt pi-coding-agent-input-window-display 'on-demand) + +;; Start sessions with only the chat window visible; open the input on +;; demand (M-x pi-coding-agent-open-input, or `i'/`a' under Evil) and +;; have it hide again after each send: +(setopt pi-coding-agent-input-window-display 'hidden) + ;; Copy source Markdown from the chat buffer instead of only visible text: (setopt pi-coding-agent-copy-raw-markdown t) diff --git a/pi-coding-agent-input.el b/pi-coding-agent-input.el index 23f7a6e..73463d7 100644 --- a/pi-coding-agent-input.el +++ b/pi-coding-agent-input.el @@ -323,9 +323,11 @@ The /compact command is handled locally; other slash commands sent to pi." (pi-coding-agent--builtin-command-name text))) (busy (pi-coding-agent--queue-followup-text chat-buf text) + (pi-coding-agent--maybe-hide-input-window) (message "Pi: Message queued (will send when Pi is ready)")) (t (pi-coding-agent--accept-input-text text) + (pi-coding-agent--maybe-hide-input-window) (with-current-buffer chat-buf (pi-coding-agent--prepare-and-send text)))))) diff --git a/pi-coding-agent-ui.el b/pi-coding-agent-ui.el index 2b73e92..17fc167 100644 --- a/pi-coding-agent-ui.el +++ b/pi-coding-agent-ui.el @@ -134,6 +134,27 @@ total window height, e.g. 0.3 means 30% for input." (float :tag "Fraction (0.0–1.0)")) :group 'pi-coding-agent) +(defcustom pi-coding-agent-input-window-display 'always + "How the input window is displayed alongside the chat window. +When `always' (the default), the input window is shown whenever the +session is displayed. + +When `on-demand', the input window is shown when a session is first +launched, hidden after each send, and reopened with +\[pi-coding-agent-open-input]. Redisplaying an existing session +\(e.g. with `pi-coding-agent-toggle') shows only the chat window. + +When `hidden', a session launches with only the chat window visible. +In every other respect this is like `on-demand': the input is hidden +after each send, reopened with `pi-coding-agent-open-input', and +redisplaying an existing session shows only the chat window. This +suits a chat-centric workflow where you compose in the input only +when needed." + :type '(choice (const :tag "Always visible" always) + (const :tag "On demand (hide after send)" on-demand) + (const :tag "Hidden at launch (chat only; open on demand)" hidden)) + :group 'pi-coding-agent) + (defcustom pi-coding-agent-activity-phase-functions nil "Functions called after a session activity phase is applied. Each function is called with five arguments: @@ -1662,25 +1683,90 @@ currently selected window." below)))) (defun pi-coding-agent--best-input-window (chat-buf input-buf) - "Return best visible window for INPUT-BUF in current frame. + "Return best visible window for INPUT-BUF in current frame, or nil. Prefer the input window below the selected CHAT-BUF window, then the selected input window, then the tallest input window." - (let* ((input-wins (get-buffer-window-list input-buf nil)) - (selected (selected-window)) - (selected-chat-win (and (eq (window-buffer selected) chat-buf) - selected))) - (or (pi-coding-agent--paired-input-window selected-chat-win input-buf) - (and (memq selected input-wins) - selected) - (pi-coding-agent--window-with-most-height input-wins)))) + (when-let* ((input-wins (get-buffer-window-list input-buf nil))) + (let* ((selected (selected-window)) + (selected-chat-win (and (eq (window-buffer selected) chat-buf) + selected))) + (or (pi-coding-agent--paired-input-window selected-chat-win input-buf) + (and (memq selected input-wins) + selected) + (pi-coding-agent--window-with-most-height input-wins))))) (defun pi-coding-agent--focus-input-window (chat-buf input-buf) "Select a visible INPUT-BUF window for the CHAT-BUF session." (when-let* ((win (pi-coding-agent--best-input-window chat-buf input-buf))) (select-window win))) -(defun pi-coding-agent--display-buffers (chat-buf input-buf) +(defun pi-coding-agent--split-input-below-chat (chat-buf input-buf) + "Show INPUT-BUF in a new window below the best visible CHAT-BUF window. +The new window is soft-dedicated so `display-buffer' never targets it. +Return the new input window, or nil when no chat window can be split." + (when-let* ((chat-win + (or (and (eq (window-buffer (selected-window)) chat-buf) + (pi-coding-agent--window-can-split-for-input-p + (selected-window)) + (selected-window)) + (cl-find-if #'pi-coding-agent--window-can-split-for-input-p + (pi-coding-agent--windows-by-height + (get-buffer-window-list chat-buf nil)))))) + (let ((input-win + (split-window + chat-win (- (pi-coding-agent--input-height-for-window chat-win)) + 'below))) + (set-window-buffer input-win input-buf) + ;; Soft-dedicate the input window so `display-buffer' never + ;; targets it (magit, help, compilation, etc.). The 'side + ;; value still allows `switch-to-buffer' and `C-x o'. + (set-window-dedicated-p input-win 'side) + input-win))) + +(defun pi-coding-agent-open-input () + "Open the input window below the chat window and select it. +If an input window is already visible, select it instead. If no chat +window is visible either, restore the full session layout." + (interactive) + (let* ((chat-buf (pi-coding-agent--get-chat-buffer)) + (input-buf (pi-coding-agent--get-input-buffer))) + (unless (and (buffer-live-p chat-buf) (buffer-live-p input-buf)) + (user-error "No pi session for this buffer")) + (cond + ((pi-coding-agent--focus-input-window chat-buf input-buf)) + ((when-let* ((input-win + (pi-coding-agent--split-input-below-chat chat-buf input-buf))) + (select-window input-win))) + (t (pi-coding-agent--display-buffers chat-buf input-buf))))) + +(defun pi-coding-agent--input-window-on-demand-p () + "Return non-nil when the input window is shown on demand. +True for the `on-demand' and `hidden' values of +`pi-coding-agent-input-window-display', which both hide the input +after each send and reopen it with `pi-coding-agent-open-input'." + (memq pi-coding-agent-input-window-display '(on-demand hidden))) + +(defun pi-coding-agent--maybe-hide-input-window () + "Hide the input window when it is shown on demand. +Intended to run after `pi-coding-agent-send' accepts input. When the +selected window is deleted, select a chat window instead." + (when (pi-coding-agent--input-window-on-demand-p) + (let* ((input-buf (pi-coding-agent--get-input-buffer)) + (input-wins (and (buffer-live-p input-buf) + (get-buffer-window-list input-buf nil))) + (selected (selected-window))) + (dolist (win input-wins) + (when (window-parent win) + (delete-window win))) + (when (and (memq selected input-wins) + (not (window-live-p selected))) + (when-let* ((chat-buf (pi-coding-agent--get-chat-buffer)) + (chat-win (get-buffer-window chat-buf))) + (select-window chat-win)))))) + +(defun pi-coding-agent--display-buffers (chat-buf input-buf &optional chat-only) "Ensure CHAT-BUF and INPUT-BUF are visible. +When CHAT-ONLY is non-nil, show only the chat window. Uses a split window with chat above and input below. Falls back to a larger window when the selected one cannot be split." (let* ((chat-wins (get-buffer-window-list chat-buf nil)) @@ -1694,20 +1780,17 @@ larger window when the selected one cannot be split." (when (and input-wins (not chat-wins)) (pi-coding-agent--delete-extra-input-windows input-wins target)) (with-selected-window target - (unless (pi-coding-agent--window-can-split-for-input-p target) - (delete-other-windows target)) - (unless (pi-coding-agent--window-can-split-for-input-p target) - (user-error "Window too small for chat + input layout")) + (unless chat-only + (unless (pi-coding-agent--window-can-split-for-input-p target) + (delete-other-windows target)) + (unless (pi-coding-agent--window-can-split-for-input-p target) + (user-error "Window too small for chat + input layout"))) (switch-to-buffer chat-buf) (with-current-buffer chat-buf (goto-char (point-max))) - (let ((input-height (pi-coding-agent--input-height-for-window target))) - (setq input-win (split-window nil (- input-height) 'below)) - (set-window-buffer input-win input-buf) - ;; Soft-dedicate the input window so `display-buffer' never - ;; targets it (magit, help, compilation, etc.). The 'side - ;; value still allows `switch-to-buffer' and `C-x o'. - (set-window-dedicated-p input-win 'side))) + (unless chat-only + (setq input-win + (pi-coding-agent--split-input-below-chat chat-buf input-buf)))) (when (window-live-p input-win) (select-window input-win)))) diff --git a/pi-coding-agent.el b/pi-coding-agent.el index 053b299..4563492 100644 --- a/pi-coding-agent.el +++ b/pi-coding-agent.el @@ -173,11 +173,15 @@ Returns the chat buffer." chat-buf)) (defun pi-coding-agent--show-session-buffers (chat-buf input-buf) - "Show CHAT-BUF and INPUT-BUF, focusing input when both are visible." + "Show CHAT-BUF and INPUT-BUF, focusing input when both are visible. +When `pi-coding-agent-input-window-display' is `hidden', a freshly +displayed session starts with only the chat window visible." (if (and (get-buffer-window-list chat-buf nil) (get-buffer-window-list input-buf nil)) (pi-coding-agent--focus-input-window chat-buf input-buf) - (pi-coding-agent--display-buffers chat-buf input-buf))) + (pi-coding-agent--display-buffers + chat-buf input-buf + (eq pi-coding-agent-input-window-display 'hidden)))) (defun pi-coding-agent--dired-regular-file-at-point () "Return Dired's regular file at point, or nil." @@ -290,9 +294,11 @@ If no session exists, signal an error." (and input-buf (get-buffer-window-list input-buf nil))) (with-current-buffer chat-buf (pi-coding-agent--hide-session-windows))) - ;; Session hidden: show it + ;; Session hidden: show it (chat only when input is shown on demand) (t - (pi-coding-agent--display-buffers chat-buf input-buf))))) + (pi-coding-agent--display-buffers + chat-buf input-buf + (pi-coding-agent--input-window-on-demand-p)))))) (provide 'pi-coding-agent) ;;; pi-coding-agent.el ends here diff --git a/test/pi-coding-agent-test.el b/test/pi-coding-agent-test.el index 3f6d7d0..b8554f7 100644 --- a/test/pi-coding-agent-test.el +++ b/test/pi-coding-agent-test.el @@ -116,7 +116,7 @@ still mock the RPC boundary, so the process is never used for I/O." (push (list 'setup-session dir session) calls) chat-buf)) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (chat input) + (lambda (chat input &optional _chat-only) (push (list 'display chat input) calls))) ((symbol-function 'pi-coding-agent--session-transition-ready-p) (lambda (chat action) @@ -184,7 +184,7 @@ still mock the RPC boundary, so the process is never used for I/O." (push (list 'setup-session dir session) calls) chat-buf)) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (chat input) + (lambda (chat input &optional _chat-only) (push (list 'display chat input) calls))) ((symbol-function 'pi-coding-agent--session-transition-ready-p) (lambda (chat action) @@ -226,7 +226,7 @@ still mock the RPC boundary, so the process is never used for I/O." ((symbol-function 'pi-coding-agent--setup-session) (lambda (_dir &optional _session) chat-buf)) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (_chat _input) (setq displayed t))) + (lambda (_chat _input &optional _chat-only) (setq displayed t))) ((symbol-function 'pi-coding-agent--session-transition-ready-p) (lambda (_chat _action) nil)) ((symbol-function 'pi-coding-agent--resume-selected-session) @@ -1127,7 +1127,7 @@ must decide whether this is a no-op." (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) - (lambda (chat _input) (setq displayed chat)))) + (lambda (chat _input &rest _) (setq displayed chat)))) (unwind-protect (progn ;; Create named session first. @@ -1303,7 +1303,7 @@ must decide whether this is a no-op." (lambda (&rest _) (ert-fail "toggle checked process dependencies"))) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (_chat _input) + (lambda (_chat _input &rest _) (setq displayed t)))) (with-temp-buffer (setq default-directory root) @@ -1323,7 +1323,7 @@ must decide whether this is a no-op." ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil)) ((symbol-function 'pi-coding-agent--check-dependencies) #'ignore) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (chat _input) + (lambda (chat _input &rest _) (setq displayed-name (buffer-name chat))))) (unwind-protect (progn @@ -1478,7 +1478,7 @@ must decide whether this is a no-op." ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil)) ((symbol-function 'pi-coding-agent--check-dependencies) #'ignore) ((symbol-function 'pi-coding-agent--display-buffers) - (lambda (chat-buf input-buf) + (lambda (chat-buf input-buf &rest _) (setq displayed-chat chat-buf displayed-input input-buf)))) (setq chat (pi-coding-agent--setup-session root nil) diff --git a/test/pi-coding-agent-ui-test.el b/test/pi-coding-agent-ui-test.el index 5b303e0..04c7620 100644 --- a/test/pi-coding-agent-ui-test.el +++ b/test/pi-coding-agent-ui-test.el @@ -644,6 +644,144 @@ Closes the category from issue #234: any instance without a usable (pi-coding-agent-test--kill-session-buffers root) (delete-other-windows))))) +(ert-deftest pi-coding-agent-test-display-buffers-chat-only-when-show-input-nil () + "SHOW-INPUT nil displays only the chat window." + (let ((root "/tmp/pi-coding-agent-test-display-chat-only/")) + (make-directory root t) + (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) + ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil))) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input t) + (should (get-buffer-window chat)) + (should-not (get-buffer-window input))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-open-input-splits-below-chat () + "`pi-coding-agent-open-input' opens a soft-dedicated input window below chat." + (let ((root "/tmp/pi-coding-agent-test-open-input-split/")) + (make-directory root t) + (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) + ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil))) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input t) + (select-window (get-buffer-window chat)) + (pi-coding-agent-open-input) + (let ((input-win (get-buffer-window input))) + (should input-win) + (should (eq (selected-window) input-win)) + (should (eq 'side (window-dedicated-p input-win))) + (should (eq (window-in-direction 'above input-win) + (get-buffer-window chat))))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-open-input-focuses-visible-input () + "`pi-coding-agent-open-input' selects an already-visible input window." + (let ((root "/tmp/pi-coding-agent-test-open-input-focus/")) + (make-directory root t) + (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) + ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil))) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input) + (select-window (get-buffer-window chat)) + (pi-coding-agent-open-input) + (should (eq (selected-window) (get-buffer-window input))) + (should (= 2 (length (window-list nil 'no-mini))))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-send-hides-input-window-on-demand () + "Sending hides the input window when display is `on-demand'." + (let ((root "/tmp/pi-coding-agent-test-send-hide/") + (pi-coding-agent-input-window-display 'on-demand)) + (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--prepare-and-send) #'ignore)) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input) + (with-current-buffer input + (insert "Hello, pi!") + (pi-coding-agent-send)) + (should-not (get-buffer-window input)) + (should (eq (selected-window) (get-buffer-window chat)))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-send-keeps-input-window-when-always () + "Sending keeps the input window when display is `always'." + (let ((root "/tmp/pi-coding-agent-test-send-keep/") + (pi-coding-agent-input-window-display 'always)) + (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--prepare-and-send) #'ignore)) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input) + (with-current-buffer input + (insert "Hello, pi!") + (pi-coding-agent-send)) + (should (get-buffer-window input))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-show-session-buffers-hidden-launches-chat-only () + "A fresh session launches chat-only when display is `hidden'. +`pi-coding-agent--show-session-buffers' honors +`pi-coding-agent-input-window-display', so a `hidden' session starts +without an input window." + (let ((root "/tmp/pi-coding-agent-test-show-hidden/") + (pi-coding-agent-input-window-display 'hidden)) + (make-directory root t) + (cl-letf (((symbol-function 'project-current) (lambda (&rest _) nil)) + ((symbol-function 'pi-coding-agent--start-process) (lambda (_) nil))) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--show-session-buffers chat input) + (should (get-buffer-window chat)) + (should-not (get-buffer-window input))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + +(ert-deftest pi-coding-agent-test-send-hides-input-window-when-hidden () + "Sending hides the input window when display is `hidden'." + (let ((root "/tmp/pi-coding-agent-test-send-hide-hidden/") + (pi-coding-agent-input-window-display 'hidden)) + (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--prepare-and-send) #'ignore)) + (unwind-protect + (let* ((chat (pi-coding-agent--setup-session root nil)) + (input (buffer-local-value 'pi-coding-agent--input-buffer chat))) + (delete-other-windows) + (pi-coding-agent--display-buffers chat input) + (with-current-buffer input + (insert "Hello, pi!") + (pi-coding-agent-send)) + (should-not (get-buffer-window input)) + (should (eq (selected-window) (get-buffer-window chat)))) + (pi-coding-agent-test--kill-session-buffers root) + (delete-other-windows))))) + (ert-deftest pi-coding-agent-test-hide-session-windows-uses-current-frame-window-list () "`pi-coding-agent--hide-session-windows' should query current frame windows only." (let ((root "/tmp/pi-coding-agent-test-hide-frame-local/")