;;; agent-shell-viewport.el --- Agent shell viewport interaction  -*- lexical-binding: t -*-

;; Copyright (C) 2025 Alvaro Ramirez

;; Author: Alvaro Ramirez https://xenodium.com
;; URL: https://github.com/xenodium/agent-shell

;; This package is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.

;; This package is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;; GNU General Public License for more details.

;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; Viewport provides an alternative interaction mode for agent-shell.
;; It enables crafting queries, navigating conversation history,
;; and viewing responses in a dedicated buffer.
;;
;; Support the work https://github.com/sponsors/xenodium

;;; Code:

(require 'cursor-sensor)
(require 'seq)
(require 'subr-x)
(require 'window)
(require 'flymake)
(require 'agent-shell-list-edit)
(require 'agent-shell-markdown)
(require 'agent-shell-faces)
(require 'shell-maker)
(require 'transient)

(eval-when-compile
  (require 'cl-lib))

(declare-function agent-shell--block-quote "agent-shell")
(declare-function agent-shell--current-shell "agent-shell")
(declare-function agent-shell--display-buffer "agent-shell")
(declare-function agent-shell--get-region "agent-shell")
(declare-function agent-shell--insert-to-shell-buffer "agent-shell")
(declare-function agent-shell--make-header "agent-shell")
(declare-function agent-shell--context "agent-shell")
(declare-function agent-shell--shell-buffer "agent-shell")
(declare-function agent-shell--state "agent-shell")
(declare-function agent-shell--prompt-queue-echo "agent-shell-prompt-queue")
(declare-function agent-shell--filter-buffer-substring "agent-shell")
(declare-function agent-shell-buffers "agent-shell")
(declare-function agent-shell-goto-last-interaction "agent-shell")
(declare-function agent-shell-interaction-at-point "agent-shell")
(declare-function agent-shell-copy-session-id "agent-shell")
(declare-function agent-shell-cycle-session-mode "agent-shell")
(declare-function agent-shell-interrupt "agent-shell")
(declare-function agent-shell-interrupt-confirmed-p "agent-shell")
(declare-function agent-shell-open-transcript "agent-shell")
(declare-function agent-shell-prompt-queue "agent-shell-prompt-queue")
(declare-function agent-shell-prompt-queue-remove "agent-shell-prompt-queue")
(declare-function agent-shell-prompt-queue-resume "agent-shell-prompt-queue")
(declare-function agent-shell-view-acp-logs "agent-shell")
(declare-function agent-shell-view-traffic "agent-shell")
(declare-function agent-shell-next-permission-button "agent-shell")
(declare-function agent-shell-other-buffer "agent-shell")
(declare-function agent-shell-previous-permission-button "agent-shell")
(declare-function agent-shell-set-session-mode "agent-shell")
(declare-function agent-shell-set-session-model "agent-shell")
(declare-function agent-shell-set-session-thought-level "agent-shell")
(declare-function agent-shell-ui-backward-block "agent-shell")
(declare-function agent-shell-ui-forward-block "agent-shell")
(declare-function agent-shell-ui-mode "agent-shell")
(declare-function agent-shell--render-markdown "agent-shell")
(declare-function agent-shell-completion-mode "agent-shell-completion")
(declare-function agent-shell-yank-dwim "agent-shell")

(defvar agent-shell-header-style)
(defvar agent-shell-prefer-viewport-interaction)
(defvar agent-shell-viewport-dismiss-on-send)
(defvar agent-shell-preferred-agent-config)
(defvar agent-shell-session-strategy)
(defvar agent-shell--state)
(defvar agent-shell-file-completion-enabled)

(defvar-local agent-shell-viewport--compose-snapshot nil
  "Alist with :content and :location from compose buffer before viewing history.")
;; The viewport buffer transitions between major modes which clears
;; buffer-local vars. Make snapshot permanent-local.
(put 'agent-shell-viewport--compose-snapshot 'permanent-local t)

(defvar-local agent-shell-viewport--peek-location nil
  "Alist with :point and :position of the peeked interaction.
Saved when replying from a peek and restored by
`agent-shell-viewport-compose-peek-last', so repeated peeks return to the
same reading position.  :position is the history position, so it is only
restored while the peeked interaction is the same one.")
;; Survives mode switches (edit <-> view) which clear buffer-local vars.
(put 'agent-shell-viewport--peek-location 'permanent-local t)

(defvar-local agent-shell-viewport--ring-index nil
  "Current index into `comint-input-ring' for history navigation.")
;; Survives mode switches (edit <-> view) which clear buffer-local vars.
(put 'agent-shell-viewport--ring-index 'permanent-local t)

(cl-defun agent-shell-viewport--show-buffer (&key append override submit no-focus shell-buffer edit)
  "Show a viewport compose buffer for the agent shell.

APPEND is appended to the viewport compose buffer.
OVERRIDE, when non-nil, replaces content verbatim (no trimming).
SUBMIT, when non-nil, submits after insertion.
NO-FOCUS, when non-nil, avoids focusing the viewport compose buffer.
SHELL-BUFFER, when non-nil, prefer this shell buffer.
EDIT, when non-nil, open in edit mode even while the shell is busy, so
the user can compose a prompt to queue (rather than staying in view mode).
NEW-SHELL, create a new shell (no history).

Returns an alist with insertion details or nil otherwise:

  ((:buffer . BUFFER)
   (:start . START)
   (:end . END))"
  (when submit
    (error "Not yet supported"))
  (when no-focus
    (error "Not yet supported"))
  (when (and append override)
    (error "Use :append or :override but not both"))
  (when-let* ((shell-buffer (or shell-buffer (agent-shell--shell-buffer)))
              (viewport-buffer (agent-shell-viewport--buffer :shell-buffer shell-buffer))
              (text (or append (agent-shell--context :shell-buffer shell-buffer) "")))
    (when (and override (not (string-empty-p text)))
      (error "Cannot override"))
    (let ((insert-start nil)
          (insert-end nil))
      (agent-shell--display-buffer viewport-buffer)
      (when (and override
                 (with-current-buffer viewport-buffer
                   ;; Only guard an in-progress compose draft.
                   (and (derived-mode-p 'agent-shell-viewport-edit-mode)
                        (not (= (buffer-size) 0)))))
        (unless (y-or-n-p "Compose buffer is not empty.  Override?")
          ;; User does not want to override.
          ;; Treat as regular text (typically appended).
          (setq text (concat text
                             (unless (string-empty-p text)
                               "\n\n")
                             override))
          (setq override nil)))
      ;; TODO: Do we need to get prompt and partial response,
      ;; in case viewport compose buffer is created for the
      ;; first time on an ongoing/busy shell session?
      (cond
       ;; Busy with no text/override to drop in -> stay in view mode.
       ;; When text/override is present, or EDIT is requested, fall through
       ;; to edit mode so the user can compose; `compose-send-*' will queue
       ;; on submit.
       ((and (not edit)
             (agent-shell-viewport--busy-p)
             (string-empty-p (string-trim text))
             (or (not override) (string-empty-p (string-trim override))))
        (agent-shell-viewport-view-mode))
       (override
        (agent-shell-viewport-edit-mode)
        (agent-shell-viewport--initialize)
        (setq insert-start (point))
        (insert override)
        (setq insert-end (point)))
       ((derived-mode-p 'agent-shell-viewport-edit-mode)
        (unless (string-empty-p text)
          (save-excursion
            (goto-char (point-max))
            (setq insert-start (point))
            (insert "\n\n" text)
            (setq insert-end (point)))))
       (t
        (agent-shell-viewport-edit-mode)
        ;; Transitioned to edit mode. Wipe content.
        (agent-shell-viewport--initialize)
        ;; Restore snapshot if needed.
        (when-let* ((snapshot agent-shell-viewport--compose-snapshot))
          (insert (map-elt snapshot :content))
          (goto-char (map-elt snapshot :location))
          (setq agent-shell-viewport--compose-snapshot nil))
        (save-excursion
          (goto-char (point-max))
          (setq insert-start (point))
          (unless (string-empty-p text)
            (insert "\n\n" text))
          (setq insert-end (point)))))
      `((:buffer . ,viewport-buffer)
        (:start . ,insert-start)
        (:end . ,insert-end)))))

(defun agent-shell-viewport-compose-send (&optional keep-composing)
  "Send the viewport composed prompt to the agent shell.

With prefix argument KEEP-COMPOSING, queue or send the prompt and keep the
compose buffer open in edit mode so another prompt can be composed and
queued right away, regardless of `agent-shell-viewport-dismiss-on-send'."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive "P")
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (when (and (not (eq agent-shell-session-strategy 'new-deferred))
             (not (with-current-buffer (agent-shell-viewport--shell-buffer)
                    (map-nested-elt agent-shell--state '(:session :id)))))
    (user-error "Session not ready... please wait"))
  (setq agent-shell-viewport--compose-snapshot nil)
  (setq agent-shell-viewport--ring-index nil)
  (setq agent-shell-viewport--peek-location nil)
  (cond
   (keep-composing
    (agent-shell-viewport--compose-queue))
   (agent-shell-viewport-dismiss-on-send
    (agent-shell-viewport-compose-send-and-dismiss))
   (agent-shell-prefer-viewport-interaction
    (agent-shell-viewport-compose-send-and-wait-for-response))
   (t
    (agent-shell-viewport-compose-send-and-kill))))

(defun agent-shell-viewport-compose-send-and-kill ()
  "Send the viewport composed prompt to the agent shell and kill compose buffer."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (let ((shell-buffer (agent-shell-viewport--shell-buffer))
        (viewport-buffer (current-buffer))
        (prompt (string-trim (buffer-string))))
    (with-current-buffer shell-buffer
      (if (agent-shell-viewport--busy-p)
          (agent-shell-prompt-queue prompt)
        (agent-shell--insert-to-shell-buffer
         :text prompt
         :submit t)))
    ;; Sending and killing viewport should not
    ;; ask user if they want to kill the shell also.
    ;; The intent is clear that they do not want that
    ;; since they are sending a prompt.
    (with-current-buffer viewport-buffer
      (let ((agent-shell-viewport--clean-up nil))
        (kill-buffer viewport-buffer)))
    (pop-to-buffer shell-buffer)))

(defun agent-shell-viewport--compose-queue ()
  "Queue or submit the composed prompt, then clear the compose buffer.

The prompt is queued when the shell is busy and submitted otherwise, so
prompts can be fired in a row.  Signals a `user-error' when the draft is
empty.  Leaves the compose buffer open in edit mode, cleared.

When the prompt is submitted immediately (not queued), it is echoed to
the minibuffer as the active prompt, since the cleared compose buffer
does not itself show the submitted prompt.  When it is queued instead,
`agent-shell-prompt-queue' already echoes the resulting queue."
  (let ((shell-buffer (agent-shell-viewport--shell-buffer))
        (prompt (string-trim (buffer-string)))
        ;; Sample busy state before `agent-shell-prompt-queue' below submits or queues.
        (queued (agent-shell-viewport--busy-p)))
    (when (string-empty-p prompt)
      (user-error "Nothing to send"))
    (with-current-buffer shell-buffer
      (agent-shell-prompt-queue prompt))
    (agent-shell-viewport--initialize)
    (unless queued
      (agent-shell--prompt-queue-echo :active-prompt prompt))))

(defun agent-shell-viewport-compose-send-and-dismiss ()
  "Queue or send the composed prompt, then dismiss the compose window.

The compose window is dismissed, restoring the previous window layout."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (let ((viewport-buffer (current-buffer)))
    (agent-shell-viewport--compose-queue)
    (agent-shell-viewport--dismiss viewport-buffer)))

(defun agent-shell-viewport--dismiss (viewport-buffer)
  "Hide VIEWPORT-BUFFER's window, restoring the previous layout.

Uses `quit-restore-window' so the window returns to what it displayed
before the compose buffer, matching `agent-shell-toggle'.  VIEWPORT-BUFFER
is not buried, so it stays recent and `agent-shell-buffers' keeps
resolving to its shell on the next invocation."
  (when-let* ((window (get-buffer-window viewport-buffer)))
    (quit-restore-window window)))

(defun agent-shell-viewport-compose-send-and-wait-for-response ()
  "Send the viewport composed prompt and display response in viewport."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (catch 'exit
    (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
      (user-error "Not in a shell viewport buffer"))
    (let ((shell-buffer (agent-shell-viewport--shell-buffer))
          (viewport-buffer (current-buffer))
          (prompt (string-trim (buffer-string))))
      (when (string-empty-p (string-trim prompt))
        (agent-shell-viewport--initialize)
        (user-error "Nothing to send"))
      (when (agent-shell-viewport--busy-p)
        (with-current-buffer shell-buffer
          (agent-shell-prompt-queue prompt))
        (with-current-buffer viewport-buffer
          (agent-shell-viewport-view-last))
        (throw 'exit nil))
      (if (derived-mode-p 'agent-shell-viewport-view-mode)
          (progn
            (agent-shell-viewport-edit-mode)
            (agent-shell-viewport--initialize))
        (agent-shell-viewport-view-mode)
        (agent-shell-viewport--initialize :prompt prompt)
        ;; (setq view-exit-action 'kill-buffer) TODO
        (when (string-equal prompt "clear")
          (agent-shell-viewport-edit-mode)
          (agent-shell-viewport--initialize))
        (agent-shell--insert-to-shell-buffer
         :shell-buffer (agent-shell-viewport--shell-buffer)
         :text prompt
         :submit t
         :no-focus t)
        ;; TODO: Point should go to beginning of response after submission.
        ;; TODO: Render prompt markdown?
        ))))

(defun agent-shell-viewport-interrupt ()
  "Interrupt active agent shell request."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (catch 'exit
    (let ((shell-buffer (agent-shell-viewport--shell-buffer)))
      (unless (agent-shell-viewport--busy-p)
        (user-error "No pending request"))
      (unless (agent-shell-interrupt-confirmed-p)
        (throw 'exit nil))
      (with-current-buffer shell-buffer
        (agent-shell-interrupt t))
      (user-error "Aborted"))))

(cl-defun agent-shell-viewport--initialize (&key prompt response)
  "Initialize viewport compose buffer.

Optionally set its PROMPT and RESPONSE."
  (agent-shell-viewport--ensure-buffer)

  ;; Recalculate and cache position
  (agent-shell-viewport--position :force-refresh t)
  (let ((inhibit-read-only t)
        (viewport-buffer (current-buffer)))
    (erase-buffer)
    (when-let* ((shell-buffer (agent-shell-viewport--shell-buffer)))
      (with-current-buffer shell-buffer
        (unless (eq agent-shell-header-style 'graphical)
          ;; Insert newline at point-min purely for
          ;; display/layout. Only needed for non-graphical header.
          (with-current-buffer viewport-buffer
            (insert (propertize "\n"
                                'cursor-intangible t
                                'front-sticky '(cursor-intangible)
                                'rear-nonsticky '(cursor-intangible)))))))
    (when prompt
      (insert
       (if (derived-mode-p 'agent-shell-viewport-view-mode)
           ;; No need to append trailing "\n\n" to split
           ;; prompt from response as the raw prompt already
           ;; carries its own trailing newline.
           ;; Give the echoed prompt the same base `line-prefix' /
           ;; `wrap-prefix' indent the response body carries (see
           ;; `agent-shell-ui--indent-text'), so the prompt lines up with
           ;; the response instead of sitting flush at column 0.
           (propertize prompt
                       'rear-nonsticky t
                       'agent-shell-viewport-prompt t
                       'line-prefix "  "
                       'wrap-prefix "  "
                       'face 'agent-shell-viewport-prompt)
         prompt)))
    (when response
      (insert response))
    ;; Re-render the header from the freshly-cached position.  Without
    ;; this, callers that set content via this function (e.g.
    ;; `agent-shell-viewport-refresh' on switch) leave the header showing
    ;; the previously-rendered position.
    (agent-shell-viewport--update-header)
    ;; TODO: Render prompt markdown?
    ))

(defun agent-shell-viewport--ensure-buffer ()
  "Ensure current buffer is a viewport and err otherwise."
  (unless (or (derived-mode-p 'agent-shell-viewport-view-mode)
              (derived-mode-p 'agent-shell-viewport-edit-mode))
    (user-error "Not in a shell viewport buffer")))

(defun agent-shell-viewport--prompt ()
  "Return the buffer prompt."
  (save-excursion
    (goto-char (point-min))
    (when-let* ((start (if (get-text-property (point-min) 'agent-shell-viewport-prompt)
                           (point-min)
                         (next-single-property-change (point-min) 'agent-shell-viewport-prompt)))
                (found (get-text-property start 'agent-shell-viewport-prompt)))
      (buffer-substring-no-properties
       start
       (or (next-single-property-change
            start 'agent-shell-viewport-prompt)
           (point-max))))))

(defun agent-shell-viewport--response ()
  "Return the buffer response."
  (save-excursion
    (goto-char (point-min))
    (when-let* ((start (if (get-text-property (point-min) 'agent-shell-viewport-prompt)
                           (point-min)
                         (next-single-property-change (point-min) 'agent-shell-viewport-prompt)))
                (found (get-text-property start 'agent-shell-viewport-prompt))
                (end (next-single-property-change start 'agent-shell-viewport-prompt)))
      (buffer-substring end (point-max)))))

(defun agent-shell-viewport--prompt-start ()
  "Return the start position of the prompt, or nil if no prompt."
  (save-excursion
    (goto-char (point-min))
    (when-let* ((start (if (get-text-property (point-min) 'agent-shell-viewport-prompt)
                          (point-min)
                        (next-single-property-change (point-min) 'agent-shell-viewport-prompt))))
      (when (get-text-property start 'agent-shell-viewport-prompt)
        start))))

(defun agent-shell-viewport--response-start ()
  "Return the start position of the response, or nil if no response."
  (save-excursion
    (goto-char (point-min))
    (when-let* ((start (if (get-text-property (point-min) 'agent-shell-viewport-prompt)
                           (point-min)
                         (next-single-property-change (point-min) 'agent-shell-viewport-prompt)))
                (found (get-text-property start 'agent-shell-viewport-prompt))
                (end (next-single-property-change start 'agent-shell-viewport-prompt)))
      (when (< end (point-max))
        end))))

(defun agent-shell-viewport-compose-cancel ()
  "Cancel prompt composition."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (setq agent-shell-viewport--compose-snapshot nil)
  (let ((viewport-buffer (current-buffer))
        (shell-buffer (agent-shell-viewport--shell-buffer)))
    (cond
     ((and agent-shell-viewport-dismiss-on-send
           (derived-mode-p 'agent-shell-viewport-edit-mode))
      (when (or (string-empty-p (string-trim (buffer-string)))
                (y-or-n-p "Discard composed prompt? "))
        (agent-shell-viewport--initialize)
        (agent-shell-viewport--dismiss viewport-buffer)))
     ;; View mode
     ((derived-mode-p 'agent-shell-viewport-view-mode)
      (bury-buffer))
     ;; Edit mode, no history to go back to
     ((with-current-buffer shell-buffer
        (not (shell-maker-history-position)))
      (when (or (string-empty-p (string-trim (buffer-string)))
                (y-or-n-p "Discard composed prompt? "))
        (agent-shell-viewport--initialize)
        (bury-buffer)))
     ;; Edit mode, has history
     (t
      (when (or (string-empty-p (string-trim (buffer-string)))
                (y-or-n-p "Discard composed prompt? "))
        (if agent-shell-prefer-viewport-interaction
            (agent-shell-viewport-view-last)
          (agent-shell-other-buffer)
          (kill-buffer viewport-buffer)))))))

(defun agent-shell-viewport-previous-history ()
  "Insert previous prompt from history into compose buffer."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (let* ((ring (with-current-buffer (agent-shell-viewport--shell-buffer)
                 (seq-filter
                  (lambda (item)
                    (not (string-empty-p item)))
                  (ring-elements comint-input-ring))))
         (next-index (unless (seq-empty-p ring)
                       (if agent-shell-viewport--ring-index
                           (1+ agent-shell-viewport--ring-index)
                         0))))
    ;; Save in-progress compose text before first history navigation.
    (when (and (not agent-shell-viewport--ring-index)
               (not agent-shell-viewport--compose-snapshot))
      (setq agent-shell-viewport--compose-snapshot
            `((:content . ,(buffer-string))
              (:location . ,(point)))))
    (cond
     ;; Empty ring.
     ((not next-index)
      (setq agent-shell-viewport--ring-index nil))
     ;; Already at oldest entry, clamp.
     ((>= next-index (seq-length ring))
      (setq agent-shell-viewport--ring-index (1- (seq-length ring))))
     (t
      (setq agent-shell-viewport--ring-index next-index)))
    (when agent-shell-viewport--ring-index
      (agent-shell-viewport--initialize
       :prompt (seq-elt ring agent-shell-viewport--ring-index)))))

(defun agent-shell-viewport-next-history ()
  "Insert next prompt from history into compose buffer."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (unless agent-shell-viewport--ring-index
    (user-error "No more history"))
  (let* ((ring (with-current-buffer (agent-shell-viewport--shell-buffer)
                 (seq-filter
                  (lambda (item)
                    (not (string-empty-p item)))
                  (ring-elements comint-input-ring))))
         (next-index (1- agent-shell-viewport--ring-index)))
    (if (< next-index 0)
        ;; Past newest entry, restore in-progress compose text.
        (let ((snapshot agent-shell-viewport--compose-snapshot))
          (setq agent-shell-viewport--ring-index nil)
          (agent-shell-viewport--initialize)
          (when snapshot
            (insert (map-elt snapshot :content))
            (goto-char (map-elt snapshot :location))
            (setq agent-shell-viewport--compose-snapshot nil)))
      ;; Show older entry.
      (setq agent-shell-viewport--ring-index next-index)
      (agent-shell-viewport--initialize
       :prompt (seq-elt ring next-index)))))

(defun agent-shell-viewport-search-history ()
  "Search prompt history, select, and insert into compose buffer."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a shell viewport buffer"))
  (insert (with-current-buffer (agent-shell-viewport--shell-buffer)
            (completing-read
             "History: "
             (delete-dups
              (seq-filter
               (lambda (item)
                 (not (string-empty-p item)))
               (ring-elements comint-input-ring))) nil t))))

(defun agent-shell-viewport-compose-peek-last ()
  "Save compose buffer snapshot and peek at the last interaction."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (user-error "Not in a prompt compose buffer"))
  (unless (with-current-buffer (agent-shell-viewport--shell-buffer)
            (shell-maker-history-position))
    (user-error "No items in history"))
  (setq agent-shell-viewport--compose-snapshot
        `((:content . ,(buffer-string))
          (:location . ,(point))))
  (agent-shell-viewport-view-last)
  ;; Return to the previous peek's reading position, but only when the
  ;; peeked interaction is unchanged.
  (when-let* ((location agent-shell-viewport--peek-location)
              ((equal (map-elt location :position)
                      (agent-shell-viewport--position :force-refresh t)))
              (pos (map-elt location :point))
              ((<= (point-min) pos (point-max))))
    (goto-char pos)))

(defun agent-shell-viewport-view-last ()
  "Display the last request/response interaction."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (agent-shell-goto-last-interaction)
  (when-let* ((current (agent-shell-interaction-at-point)))
    (agent-shell-viewport-view-mode)
    (agent-shell-viewport--initialize
     :prompt (map-elt current :prompt)
     :response (map-elt current :response))
    (goto-char (point-min))
    current))

(defun agent-shell-viewport-refresh ()
  "Refresh viewport buffer content with current item from shell."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (when-let* ((viewport-buffer (current-buffer))
              (current (agent-shell-interaction-at-point)))
    (agent-shell-viewport--initialize
     :prompt (map-elt current :prompt)
     :response (map-elt current :response))
    (goto-char (point-min))
    current))

(defun agent-shell-viewport-next-item ()
  "Go to next item.

When point is inside a rendered markdown table, navigate to the
next table cell instead.  If at point-max, attempt to switch to
next interaction."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (error "Not in a viewport buffer"))
  (if (get-text-property (point) 'agent-shell-markdown-table-source)
      (agent-shell-markdown-table-next-cell)
    (let* ((current-pos (point))
           (prompt-start (agent-shell-viewport--prompt-start))
           (response-start (agent-shell-viewport--response-start))
           (block-pos (save-mark-and-excursion
                        (agent-shell-ui-forward-block)))
           (button-pos (save-mark-and-excursion
                         (agent-shell-next-permission-button)))
           ;; Filter positions to only those after current position
           (candidates (delq nil (list
                                  (when (and prompt-start (> prompt-start current-pos))
                                    prompt-start)
                                  (when (and response-start (> response-start current-pos))
                                    response-start)
                                  block-pos
                                  button-pos)))
           (next-pos (if candidates
                         (apply #'min candidates)
                       ;; No more items, try point-max if not already there
                       (when (< current-pos (point-max))
                         (point-max)))))
      (if next-pos
          (progn
            (deactivate-mark)
            (goto-char next-pos))
        ;; At point-max with no more items, try next interaction
        (condition-case nil
            (agent-shell-viewport-next-page)
          (error
           ;; At the end of all interactions, stay at point-max
           nil))))))

(defun agent-shell-viewport-previous-item ()
  "Go to previous item.

When point is inside a rendered markdown table, navigate to the
previous table cell instead.  If at the first item, attempt to
switch to previous interaction."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (error "Not in a viewport buffer"))
  (if (get-text-property (point) 'agent-shell-markdown-table-source)
      (agent-shell-markdown-table-previous-cell)
    (let* ((current-pos (point))
           (prompt-start (agent-shell-viewport--prompt-start))
           (response-start (agent-shell-viewport--response-start))
           (block-pos (save-mark-and-excursion
                        (let ((pos (agent-shell-ui-backward-block)))
                          (when (and pos (< pos current-pos))
                            pos))))
           (button-pos (save-mark-and-excursion
                         (let ((pos (agent-shell-previous-permission-button)))
                           (when (and pos (< pos current-pos))
                             pos))))
           ;; Filter positions to only those before current position
           (candidates (delq nil (list
                                  (when (and prompt-start (< prompt-start current-pos))
                                    prompt-start)
                                  (when (and response-start (< response-start current-pos))
                                    response-start)
                                  block-pos
                                  button-pos)))
           (next-pos (when candidates
                       (apply #'max candidates))))
      (if next-pos
          (progn
            (deactivate-mark)
            (goto-char next-pos))
        ;; No more items before current position, try previous interaction
        (condition-case nil
            ;; Switch to previous page and stop at point-max (call next-interaction directly)
            (agent-shell-viewport-next-page :backwards t)
          (error
           ;; At the beginning of all interactions, stay at first item
           (when prompt-start
             (goto-char prompt-start))))))))

(defconst agent-shell-viewport--suffix " [viewport]"
  "Suffix appended to shell buffer name to create viewport buffer name.")

(cl-defun agent-shell-viewport--buffer (&key shell-buffer existing-only)
  "Get the viewport buffer associated with a SHELL-BUFFER.

With EXISTING-ONLY, only return existing buffers without creating."
  (when-let* ((shell-buffer (or shell-buffer
                                (agent-shell--shell-buffer))))
    (with-current-buffer shell-buffer
      (let* ((viewport-buffer-name (concat (buffer-name (get-buffer shell-buffer))
                                           agent-shell-viewport--suffix))
             (viewport-buffer (get-buffer viewport-buffer-name)))
        (if viewport-buffer
            viewport-buffer
          (if existing-only
              nil
            (with-current-buffer (get-buffer-create viewport-buffer-name)
              (agent-shell-viewport-edit-mode)
              (current-buffer))))))))

(cl-defun agent-shell-viewport--setup-reply (&key quoted-text)
  "Set up the buffer to compose a reply.

QUOTED-TEXT is inserted as a block quote as part of the reply."
  ;; Remember the reading position so a later peek can return to it.
  (setq agent-shell-viewport--peek-location
        `((:point . ,(point))
          (:position . ,(agent-shell-viewport--position :force-refresh t))))
  (with-current-buffer (agent-shell-viewport--shell-buffer)
    (goto-char (point-max)))
  (let ((snapshot agent-shell-viewport--compose-snapshot))
    (agent-shell-viewport-edit-mode)
    (agent-shell-viewport--initialize)
    (when snapshot
      (insert (map-elt snapshot :content))
      (setq agent-shell-viewport--compose-snapshot nil))
    (when quoted-text
      (goto-char (point-max))
      (insert (if snapshot "\n\n" "")
              (agent-shell--block-quote quoted-text) "\n\n"))
    ;; Skip past any cursor-intangible layout text (e.g. the
    ;; newline inserted by `agent-shell-viewport--initialize')
    ;; so callers like `agent-shell-viewport-reply-1' can insert.
    (goto-char (if (or snapshot quoted-text)
                   (point-max)
                 (or (next-single-property-change (point-min) 'cursor-intangible)
                     (point-max))))))

(defun agent-shell-viewport-reply ()
  "Reply as a follow-up and compose another prompt/query."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (user-error "Not in a shell viewport buffer"))
  (let ((region (map-elt (agent-shell--get-region :deactivate t) :content)))
    (agent-shell-viewport--setup-reply
     :quoted-text (when region (string-trim region))))
  ;; Setting point isn't enough at times. Force scrolling.
  (set-window-start (selected-window) (point-min)))

(defun agent-shell-viewport-quote-reply ()
  "Reply with the entire response block-quoted."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (user-error "Not in a shell viewport buffer"))
  (let ((response (or (agent-shell-viewport--response) "")))
    (agent-shell-viewport--setup-reply
     :quoted-text (string-trim (substring-no-properties response)))))

(defun agent-shell-viewport-reply-yes ()
  "Reply with \"yes\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "yes")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-1 ()
  "Reply with \"1\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "1")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-2 ()
  "Reply with \"2\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "2")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-3 ()
  "Reply with \"3\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "3")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-4 ()
  "Reply with \"4\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "4")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-5 ()
  "Reply with \"5\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "5")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-6 ()
  "Reply with \"6\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "6")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-7 ()
  "Reply with \"7\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "7")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-8 ()
  "Reply with \"8\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "8")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-9 ()
  "Reply with \"9\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "9")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-more ()
  "Reply with \"more\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "more")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-again ()
  "Reply with \"again\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "again")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-reply-continue ()
  "Reply with \"continue\" and send immediately."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy, please wait"))
  (agent-shell-viewport-reply)
  (insert "continue")
  (agent-shell-viewport-compose-send))

(defun agent-shell-viewport-previous-page ()
  "Show previous interaction (request / response)."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (agent-shell-viewport-next-page :backwards t :start-at-top t))

(cl-defun agent-shell-viewport-next-page (&key backwards start-at-top)
  "Show next interaction (request / response).

If BACKWARDS is non-nil, go to previous interaction.
If START-AT-TOP is non-nil, position at point-min regardless of direction.

If there are no more next items and a compose snapshot exists, restore the
buffer from the snapshot and switch to edit mode."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (error "Not in a viewport buffer"))
  (when (agent-shell-viewport--busy-p)
    (user-error "Busy... please wait"))
  (let ((shell-buffer (agent-shell-viewport--shell-buffer))
        (snapshot agent-shell-viewport--compose-snapshot)
        (pos (agent-shell-viewport--position :force-refresh t)))
    ;; Check if at last position going forward with a snapshot to restore
    (if (and (not backwards) snapshot pos
             (= (map-elt pos :current) (map-elt pos :total)))
        (progn
          (agent-shell-viewport-edit-mode)
          (agent-shell-viewport--initialize)
          (insert (map-elt snapshot :content))
          (goto-char (map-elt snapshot :location))
          (setq agent-shell-viewport--compose-snapshot nil)
          (cl-return-from agent-shell-viewport-next-page))
      (when-let* ((next (with-current-buffer shell-buffer
                          (if backwards
                              (progn
                                ;; Navigate relative to the interaction
                                ;; containing point, not wherever point happens
                                ;; to sit within it.  Without this, switching to
                                ;; the viewport with point mid-interaction makes
                                ;; the first backward step land on the current
                                ;; interaction's prompt instead of the previous
                                ;; interaction.
                                (goto-char (shell-maker--prompt-begin-position))
                                (when (save-excursion
                                        (let ((orig-line (line-number-at-pos)))
                                          (comint-previous-prompt 1)
                                          (= orig-line (line-number-at-pos))))
                                  (error "No previous page")))
                            (when (save-excursion
                                    (let ((orig-line (point)))
                                      (comint-next-prompt 1)
                                      (= orig-line (point))))
                              (error "No next page")))
                          (shell-maker-next-command-and-response backwards :trimmed nil))))
        (agent-shell-viewport--initialize
         :prompt (car next) :response (cdr next))
        (goto-char (if start-at-top
                       (point-min)
                     (if backwards (point-max) (point-min))))
        next))))

(defun agent-shell-viewport-set-session-model ()
  "Set session model."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let* ((shell-buffer (or (agent-shell--current-shell)
                           (user-error "Not in an agent-shell buffer")))
         (viewport-buffer (agent-shell-viewport--buffer
                           :shell-buffer shell-buffer
                           :existing-only t)))
    (with-current-buffer shell-buffer
      (agent-shell-set-session-model
       (lambda ()
         (with-current-buffer viewport-buffer
           (agent-shell-viewport--update-header)))))))

(defun agent-shell-viewport-set-session-mode ()
  "Set session mode."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let* ((shell-buffer (or (agent-shell--current-shell)
                           (user-error "Not in an agent-shell buffer")))
         (viewport-buffer (agent-shell-viewport--buffer
                           :shell-buffer shell-buffer
                           :existing-only t)))
    (with-current-buffer shell-buffer
      (agent-shell-set-session-mode
       (lambda ()
         (when viewport-buffer
           (with-current-buffer viewport-buffer
             (agent-shell-viewport--update-header))))))))

(defun agent-shell-viewport-set-session-thought-level ()
  "Set thought level (reasoning effort) for the current session."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let* ((shell-buffer (or (agent-shell--current-shell)
                           (user-error "Not in an agent-shell buffer")))
         (viewport-buffer (agent-shell-viewport--buffer
                          :shell-buffer shell-buffer
                          :existing-only t)))
    (with-current-buffer shell-buffer
      (agent-shell-set-session-thought-level
       (lambda ()
         (when viewport-buffer
           (with-current-buffer viewport-buffer
             (agent-shell-viewport--update-header))))))))

(defun agent-shell-viewport-cycle-session-mode ()
  "Cycle through available session modes."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let* ((shell-buffer (or (agent-shell--current-shell)
                           (user-error "Not in an agent-shell buffer")))
         (viewport-buffer (agent-shell-viewport--buffer
                           :shell-buffer shell-buffer
                           :existing-only t)))
    (with-current-buffer shell-buffer
      (agent-shell-cycle-session-mode
       (lambda ()
         (when viewport-buffer
           (with-current-buffer viewport-buffer
             (agent-shell-viewport--update-header))))))))

(defun agent-shell-viewport-view-traffic ()
  "View agent shell traffic buffer."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (agent-shell-view-traffic))))

(defun agent-shell-viewport-view-acp-logs ()
  "View agent shell ACP logs buffer."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (agent-shell-view-acp-logs))))

;; The viewport queueing commands were renamed to the
;; `agent-shell-viewport-prompt-queue' namespace.  A package upgrade
;; reloads this file into a running session without unbinding the old
;; names, leaving them bound to stale definitions.  Unbind them so they
;; no longer show up in `M-x' or run outdated code.
;; TODO: Remove after 2026-08-28.
(dolist (command '(agent-shell-viewport-queue-request
                   agent-shell-viewport-resume-pending-requests
                   agent-shell-viewport-remove-pending-request))
  (fmakunbound command))

(defun agent-shell-viewport-prompt-queue ()
  "Queue or immediately send a prompt depending on shell busy state."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (call-interactively #'agent-shell-prompt-queue))))

(defun agent-shell-viewport-prompt-queue-resume ()
  "Resume processing pending prompts in the queue."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (agent-shell-prompt-queue-resume))))

(defun agent-shell-viewport-prompt-queue-remove ()
  "Remove pending prompts."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (call-interactively #'agent-shell-prompt-queue-remove))))

(defun agent-shell-viewport-copy-session-id ()
  "Copy the current session ID to the kill ring."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (agent-shell-copy-session-id))))

(defun agent-shell-viewport-open-transcript ()
  "Open the transcript file for the current `agent-shell' session."
  (declare (modes agent-shell-viewport-view-mode
                  agent-shell-viewport-edit-mode))
  (interactive)
  (agent-shell-viewport--ensure-buffer)
  (let ((shell-buffer (or (agent-shell--current-shell)
                          (user-error "Not in an agent-shell buffer"))))
    (with-current-buffer shell-buffer
      (agent-shell-open-transcript))))

;; Continuously fetching position can get expensive. Cache it.
(defvar-local agent-shell-viewport--position-cache nil
  "Cached position alist with :current and :total.")

(cl-defun agent-shell-viewport--position (&key force-refresh)
  "Return the position in history of the shell buffer.

When FORCE-REFRESH is non-nil, recalculate and update cache."
  (agent-shell-viewport--ensure-buffer)
  (if (and (not force-refresh) agent-shell-viewport--position-cache)
      agent-shell-viewport--position-cache
    (let ((position (with-current-buffer (agent-shell-viewport--shell-buffer)
                      (shell-maker-history-position))))
      (setq agent-shell-viewport--position-cache position)
      position)))

(cl-defun agent-shell-viewport--busy-p (&key viewport-buffer)
  "Return non-nil if the associated shell buffer is busy.

VIEWPORT-BUFFER is the viewport buffer to check."
  (when-let* ((shell-buffer (agent-shell--shell-buffer
                             :viewport-buffer viewport-buffer
                             :no-error t)))
    (with-current-buffer shell-buffer
      shell-maker--busy)))

(defvar agent-shell-viewport-edit-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") #'agent-shell-viewport-compose-send)
    (define-key map (kbd "C-c C-p") #'agent-shell-viewport-compose-peek-last)
    (define-key map (kbd "C-c C-k") #'agent-shell-viewport-compose-cancel)
    (define-key map (kbd "C-c C-h") #'agent-shell-viewport-compose-help-menu)
    (define-key map (kbd "C-<tab>") #'agent-shell-viewport-cycle-session-mode)
    (define-key map (kbd "C-c C-m") #'agent-shell-viewport-set-session-mode)
    (define-key map (kbd "C-c C-v") #'agent-shell-viewport-set-session-model)
    (define-key map (kbd "C-c C-t") #'agent-shell-viewport-set-session-thought-level)
    (define-key map (kbd "C-c C-o") #'agent-shell-other-buffer)
    (define-key map (kbd "M-p") #'agent-shell-viewport-previous-history)
    (define-key map (kbd "M-n") #'agent-shell-viewport-next-history)
    (define-key map (kbd "M-r") #'agent-shell-viewport-search-history)
    (define-key map [remap yank] #'agent-shell-yank-dwim)
    map)
  "Keymap for `agent-shell-viewport-edit-mode'.")

(defvar agent-shell-viewport-view-mode-map
  (let ((map (make-sparse-keymap)))
    (define-key map (kbd "C-c C-c") #'agent-shell-viewport-interrupt)
    (define-key map (kbd "TAB") #'agent-shell-viewport-next-item)
    (define-key map (kbd "<backtab>") #'agent-shell-viewport-previous-item)
    (define-key map (kbd "n") #'agent-shell-viewport-next-item)
    (define-key map (kbd "p") #'agent-shell-viewport-previous-item)
    (define-key map (kbd "f") #'agent-shell-viewport-next-page)
    (define-key map (kbd "b") #'agent-shell-viewport-previous-page)
    (define-key map (kbd "r") #'agent-shell-viewport-reply)
    (define-key map (kbd "R") #'agent-shell-viewport-quote-reply)
    (define-key map (kbd "y") #'agent-shell-viewport-reply-yes)
    (define-key map (kbd "1") #'agent-shell-viewport-reply-1)
    (define-key map (kbd "2") #'agent-shell-viewport-reply-2)
    (define-key map (kbd "3") #'agent-shell-viewport-reply-3)
    (define-key map (kbd "4") #'agent-shell-viewport-reply-4)
    (define-key map (kbd "5") #'agent-shell-viewport-reply-5)
    (define-key map (kbd "6") #'agent-shell-viewport-reply-6)
    (define-key map (kbd "7") #'agent-shell-viewport-reply-7)
    (define-key map (kbd "8") #'agent-shell-viewport-reply-8)
    (define-key map (kbd "9") #'agent-shell-viewport-reply-9)
    (define-key map (kbd "q") #'bury-buffer)
    (define-key map (kbd "C-<tab>") #'agent-shell-viewport-cycle-session-mode)
    (define-key map (kbd "v") #'agent-shell-viewport-set-session-model)
    (define-key map (kbd "m") #'agent-shell-viewport-reply-more)
    (define-key map (kbd "a") #'agent-shell-viewport-reply-again)
    (define-key map (kbd "c") #'agent-shell-viewport-reply-continue)
    (define-key map (kbd "s") #'agent-shell-viewport-set-session-mode)
    (define-key map (kbd "t") #'agent-shell-viewport-set-session-thought-level)
    (define-key map (kbd "o") #'agent-shell-other-buffer)
    (define-key map (kbd "C-c C-o") #'agent-shell-other-buffer)
    (define-key map (kbd "?") #'agent-shell-viewport-help-menu)
    map)
  "Keymap for `agent-shell-viewport-view-mode'.")

(transient-define-prefix agent-shell-viewport--help-menu ()
  "`agent-shell' viewport help menu"
  [:class transient-columns
          :setup-children
          (lambda (_)
            (transient-parse-suffixes
             'agent-shell-viewport-help-menu
             (list
              (apply #'vector "Viewport Help"
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-view-mode-map
                      '(((:function . agent-shell-viewport-next-item)
                         (:description . "Next item"))
                        ((:function . agent-shell-viewport-previous-item)
                         (:description . "Previous item"))
                        ((:function . agent-shell-viewport-next-page)
                         (:description . "Next page")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-previous-page)
                         (:description . "Previous Page")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-other-buffer)
                         (:description . "Switch to shell")
                         (:transient . nil))
                        ((:function . bury-buffer)
                         (:description . "Close")
                         (:transient . nil)))))
              (apply #'vector ""
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-view-mode-map
                      `(((:function . agent-shell-viewport-reply)
                         (:description . ,(if (agent-shell-viewport--busy-p)
                                              "Queue reply…"
                                            "Reply…")))
                        ((:function . agent-shell-viewport-quote-reply)
                         (:description . "Quote reply…"))
                        ((:function . agent-shell-viewport-reply-yes)
                         (:description . "Reply \"yes\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-reply-more)
                         (:description . "Reply \"more\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-reply-again)
                         (:description . "Reply \"again\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-reply-continue)
                         (:description . "Reply \"continue\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-reply-1)
                         (:description . "Reply \"1\"")
                         (:if-not . agent-shell-viewport--busy-p)))))
              (apply #'vector ""
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-view-mode-map
                      '(((:function . agent-shell-viewport-reply-2)
                         (:description . "Reply \"2\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-reply-3)
                         (:description . "Reply \"3\"")
                         (:if-not . agent-shell-viewport--busy-p))
                        ((:function . agent-shell-viewport-set-session-model)
                         (:description . "Set model"))
                        ((:function . agent-shell-viewport-set-session-mode)
                         (:description . "Set mode"))
                        ((:function . agent-shell-viewport-set-session-thought-level)
                         (:description . "Set thought level"))
                        ((:function . agent-shell-viewport-cycle-session-mode)
                         (:description . "Cycle mode"))
                        ((:function . agent-shell-viewport-interrupt)
                         (:description . "Interrupt")))))
              (apply #'vector ""
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-view-mode-map
                      '(((:function . agent-shell-viewport-view-traffic)
                         (:description . "View traffic"))
                        ((:function . agent-shell-viewport-view-acp-logs)
                         (:description . "View logs"))
                        ((:function . agent-shell-viewport-copy-session-id)
                         (:description . "Copy session ID"))
                        ((:function . agent-shell-viewport-open-transcript)
                         (:description . "Open transcript")))))
              )))])

(defun agent-shell-viewport-help-menu ()
  "Show viewport and display the transient help menu (bound to ? in view mode)."
  (declare (modes agent-shell-viewport-view-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-view-mode)
    (error "Not in a viewport buffer"))
  (call-interactively #'agent-shell-viewport--help-menu))

(transient-define-prefix agent-shell-viewport--compose-help-menu ()
  "`agent-shell' viewport compose help menu"
  [:class transient-columns
          :setup-children
          (lambda (_)
            (transient-parse-suffixes
             'agent-shell-viewport-compose-help-menu
             (list
              (apply #'vector "Compose Help"
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-edit-mode-map
                      '(((:function . agent-shell-viewport-compose-send)
                         (:description . "Submit"))
                        ((:function . agent-shell-viewport-compose-cancel)
                         (:description . "Cancel"))
                        ((:function . agent-shell-viewport-compose-peek-last)
                         (:description . "Previous Page")))))
              (apply #'vector ""
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-edit-mode-map
                      '(((:function . agent-shell-viewport-previous-history)
                         (:description . "Previous prompt"))
                        ((:function . agent-shell-viewport-next-history)
                         (:description . "Next prompt"))
                        ((:function . agent-shell-viewport-search-history)
                         (:description . "Search prompts")))))
              (apply #'vector ""
                     (agent-shell-viewport--make-transient-group
                      agent-shell-viewport-edit-mode-map
                      '(((:function . agent-shell-viewport-set-session-model)
                         (:description . "Set model"))
                        ((:function . agent-shell-viewport-set-session-mode)
                         (:description . "Set mode"))
                        ((:function . agent-shell-viewport-set-session-thought-level)
                         (:description . "Set thought level"))
                        ((:function . agent-shell-viewport-cycle-session-mode)
                         (:description . "Cycle mode"))
                        ((:function . agent-shell-other-buffer)
                         (:description . "Switch to shell")
                         (:transient . nil))))))))])

(defun agent-shell-viewport-compose-help-menu ()
  "Show the transient help menu for compose (edit) mode."
  (declare (modes agent-shell-viewport-edit-mode))
  (interactive)
  (unless (derived-mode-p 'agent-shell-viewport-edit-mode)
    (error "Not in a compose buffer"))
  (call-interactively #'agent-shell-viewport--compose-help-menu))

(defun agent-shell-viewport--make-transient-group (keymap commands)
  "Build a list of transient suffix specs from COMMANDS using KEYMAP.
Each element of COMMANDS is an alist with keys :function, :description,
and optionally :if, :if-not, or :transient (defaults to t).
Returns only suffixes whose function has a binding in KEYMAP."
  (seq-filter
   #'identity
   (seq-map (lambda (command)
              (when-let* ((keys (where-is-internal (map-elt command :function)
                                                   keymap t))
                          ((not (keymapp keys)))
                          (description (map-elt command :description)))
                (append (list (key-description keys) description (map-elt command :function)
                              :transient (map-elt command :transient t))
                        (when-let* ((pred (map-elt command :if)))
                          (list :if pred))
                        (when-let* ((pred (map-elt command :if-not)))
                          (list :if-not pred)))))
            commands)))

(defun agent-shell-viewport--update-header ()
  "Update header and mode line based on `agent-shell-header-style'.

Automatically determines position, status, key hints and menu keys based
on current major mode."
  (agent-shell-viewport--ensure-buffer)
  (let* ((pos (or (agent-shell-viewport--position)
                  (list (cons :current 1) (cons :total 1))))
         (position-label (format "%d/%d" (map-elt pos :current) (map-elt pos :total)))
         (status (cond
                  ((and (agent-shell-viewport--busy-p)
                        (derived-mode-p 'agent-shell-viewport-edit-mode))
                   (propertize "Edit (queue)" 'face 'agent-shell-viewport-status-edit))
                  ((agent-shell-viewport--busy-p) (propertize "Busy" 'face 'agent-shell-viewport-status-busy))
                  ((derived-mode-p 'agent-shell-viewport-edit-mode)
                   (propertize "Edit" 'face 'agent-shell-viewport-status-edit))
                  ((derived-mode-p 'agent-shell-viewport-view-mode)
                   (propertize "View" 'face 'agent-shell-viewport-status-view))))
         (key-hints (cond
                    ((derived-mode-p 'agent-shell-viewport-edit-mode)
                     (list
                      `((:key . ,(key-description (where-is-internal
                                                   'agent-shell-viewport-compose-send
                                                   agent-shell-viewport-edit-mode-map t)))
                        (:description . "Submit"))
                      `((:key . ,(key-description (where-is-internal
                                                   'agent-shell-viewport-compose-cancel
                                                   agent-shell-viewport-edit-mode-map t)))
                        (:description . "Cancel"))
                      `((:key . ,(key-description (where-is-internal
                                                   'agent-shell-viewport-compose-peek-last
                                                   agent-shell-viewport-edit-mode-map t)))
                        (:description . "Previous Page"))
                      `((:key . ,(key-description (where-is-internal
                                                   'agent-shell-viewport-compose-help-menu
                                                   agent-shell-viewport-edit-mode-map t)))
                        (:description . "Help"))))
                    ((derived-mode-p 'agent-shell-viewport-view-mode)
                     (append
                      (list
                       `((:key . ,(key-description (where-is-internal
                                                    'agent-shell-viewport-next-item
                                                    agent-shell-viewport-view-mode-map t)))
                         (:description . "Next"))
                       `((:key . ,(key-description (where-is-internal
                                                    'agent-shell-viewport-previous-item
                                                    agent-shell-viewport-view-mode-map t)))
                         (:description . "Previous")))
                      (list
                       `((:key . ,(key-description (where-is-internal
                                                    'agent-shell-viewport-reply
                                                    agent-shell-viewport-view-mode-map t)))
                         (:description . ,(if (agent-shell-viewport--busy-p)
                                              "Queue reply…"
                                            "Reply…"))))
                      (when (agent-shell-viewport--busy-p)
                        (list
                         `((:key . ,(key-description (where-is-internal
                                                      'agent-shell-viewport-interrupt
                                                      agent-shell-viewport-view-mode-map t)))
                           (:description . "Interrupt"))))
                      (list
                       `((:key . ,(key-description (where-is-internal
                                                    'agent-shell-viewport-help-menu
                                                    agent-shell-viewport-view-mode-map t)))
                         (:description . "Help")))))))
         (keymap (cond
                  ((derived-mode-p 'agent-shell-viewport-edit-mode)
                   agent-shell-viewport-edit-mode-map)
                  ((derived-mode-p 'agent-shell-viewport-view-mode)
                   agent-shell-viewport-view-mode-map)))
         (model-binding (when keymap
                          (key-description (where-is-internal
                                            'agent-shell-viewport-set-session-model
                                            keymap t))))
         (mode-binding (when keymap
                         (key-description (where-is-internal
                                           'agent-shell-viewport-set-session-mode
                                           keymap t))))
         (thought-level-binding (when keymap
                                  (key-description (where-is-internal
                                                    'agent-shell-viewport-set-session-thought-level
                                                    keymap t)))))
    (when-let* ((shell-buffer (agent-shell-viewport--shell-buffer))
                (header (with-current-buffer shell-buffer
                          (agent-shell--make-header (agent-shell--state)
                                                    :position position-label
                                                    :status status
                                                    :key-hints key-hints
                                                    :menu-keys `((:model . ,model-binding)
                                                                 (:mode . ,mode-binding)
                                                                 (:thought-level . ,thought-level-binding))))))
      (setq-local header-line-format header))))

(defvar-local agent-shell-viewport--clean-up t)

(cl-defun agent-shell-viewport--shell-buffer (&optional viewport-buffer)
  "Get the shell buffer associated with VIEWPORT-BUFFER.

Derives shell buffer name by removing the viewport suffix from buffer name.
Returns nil if VIEWPORT-BUFFER is not a viewport buffer or shell doesn't exist."
  (when-let* ((viewport-name (buffer-name (or viewport-buffer (current-buffer))))
              ((string-suffix-p agent-shell-viewport--suffix viewport-name))
              (shell-name (substring viewport-name 0
                                     (- (length viewport-name)
                                        (length agent-shell-viewport--suffix)))))
    (get-buffer shell-name)))

(defun agent-shell-viewport--clean-up ()
  "Clean up resources.

For example, offer to kill associated shell session."
  (agent-shell-viewport--ensure-buffer)
  (if (and agent-shell-viewport--clean-up
           ;; Only offer to kill shell buffers when viewport buffer
           ;; is explicitly being killed from a viewport buffer.
           (eq (current-buffer)
               (window-buffer (selected-window))))
      ;; Temporarily disable cleaning up to avoid multiple clean-ups
      ;; triggered by shell buffers attempting to kill viewport buffer.
      (let ((agent-shell-viewport--clean-up nil))
        (when-let* ((shell-buffers (seq-filter (lambda (shell-buffer)
                                                 (and (equal (agent-shell-viewport--buffer
                                                              :shell-buffer shell-buffer
                                                              :existing-only t)
                                                             (current-buffer))
                                                      ;; Skip shells already shutting down (client
                                                      ;; is nil after agent-shell--shutdown).
                                                      (buffer-local-value 'agent-shell--state shell-buffer)
                                                      (map-elt (buffer-local-value 'agent-shell--state shell-buffer) :client)))
                                               (agent-shell-buffers)))
                    ((y-or-n-p "Kill shell session too?")))
          (mapc (lambda (shell-buffer)
                  (kill-buffer shell-buffer))
                shell-buffers)))))

(define-derived-mode agent-shell-viewport-edit-mode text-mode "Agent Shell Viewport (Edit)"
  "Major mode for composing agent shell prompts.

\\{agent-shell-viewport-edit-mode-map}"
  (cursor-intangible-mode +1)
  (setq buffer-read-only nil)
  ;; Indent composed text to match the response body's base `line-prefix'
  ;; (see `agent-shell-ui--indent-text') and the echoed prompt in view
  ;; mode, so submitting doesn't jump the content sideways.  The buffer
  ;; variables apply to every typed line and are cleared on the next
  ;; major-mode change, so view mode keeps its own text-property prefixes.
  (setq-local line-prefix "  ")
  (setq-local wrap-prefix "  ")
  (when agent-shell-file-completion-enabled
    (agent-shell-completion-mode +1))
  (agent-shell-list-edit-mode +1)
  (agent-shell-viewport--update-header)
  (let ((inhibit-read-only t))
    (erase-buffer))
  (add-hook 'kill-buffer-hook #'agent-shell-viewport--clean-up nil t))

(define-derived-mode agent-shell-viewport-view-mode text-mode "Agent Shell Viewport (View)"
  "Major mode for viewing agent shell prompts (read-only).

\\{agent-shell-viewport-view-mode-map}"
  (cursor-intangible-mode +1)
  (agent-shell-ui-mode +1)
  (add-hook 'agent-shell-ui-post-expand-fragment-at-point-hook
            #'agent-shell--render-markdown nil t)
  (agent-shell-viewport--update-header)
  (setq-local filter-buffer-substring-function #'agent-shell--filter-buffer-substring)
  (setq buffer-read-only t)
  (add-hook 'kill-buffer-hook #'agent-shell-viewport--clean-up nil t))

(provide 'agent-shell-viewport)

;;; agent-shell-viewport.el ends here
