Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ A soft fork of [[https://github.com/xenodium/agent-shell][agent-shell]] with ext
- Live-validate workflow doc (=.agents/commands/live-validate.md=) describing the batch-mode rendering verification used for rendering-pipeline changes ([[https://github.com/timvisher-dd/agent-shell-plus/pull/7][#7]])
- =gfm-mode= compose buffer for the interactive =agent-shell-queue-request=, replacing the read-string minibuffer prompt (non-interactive callers still pass =PROMPT= directly) ([[https://github.com/timvisher-dd/agent-shell-plus/pull/9][#9]])
- =agent-shell-resume-session= repurposed from an ID-prompt command into a session-picker entry point: opens the same fuzzy-completion picker that =agent-shell-session-strategy= ='prompt= uses, and force-shows session IDs in the picker (via a new =:show-session-id= keyword on =agent-shell--start=) so users with an ID in hand can find it via their completion framework ([[https://github.com/timvisher-dd/agent-shell-plus/pull/11][#11]])
- New =agent-shell-quote-reply= command (bound to =q= in the =agent-shell-help-menu= "Insert" group) pops the queue-compose buffer prefilled with the agent's last prose reply wrapped as a GFM block quote. Filters out tool-call drawers and thought blocks by walking back to the most recent =agent_message_chunk= fragment instead of using =shell-maker--command-and-response-at-point=, which would otherwise include rendered drawer text in the quoted output. =agent-shell-queue-request= now also gathers any DWIM context available in the source buffer (active region, error at point, dired marked files) and prefills the compose buffer with it; regions from buffers with no file association are wrapped as GFM block quotes while file-visiting and dired context keep their existing purpose-built decoration
- Fix queued-request submit stranding point: when a queued request submits (auto-resume on turn completion, or interactive =C-c C-c=), point no longer lands at the end of the last agent message above the new prompt. =agent-shell--insert-to-shell-buffer= parked point at the start of the inserted text (the DWIM "land on your context" behavior), but for the =:submit t= path that text becomes transcript above the new prompt — and shell-maker only auto-scrolls streamed output while point is at =point-max=, so the cursor stayed stranded there for the whole turn. Submit now leaves point at =point-max= so it tracks the prompt; the DWIM start-of-context placement is kept only for non-submit insertions
- Fix interactive =RET= submit stranding point above the streamed reply: when you submit a prompt by typing at the shell prompt and pressing =RET=, =shell-maker-submit= leaves point at the response boundary just below the echoed input rather than at =point-max= (because =shell-maker-point-at-last-prompt-p= is true, it skips its own =goto-char=), so the streamed reply and new prompt land below point and the cursor is stranded above them for the whole turn. =agent-shell--handle= (the =:execute-command= callback) now snaps point to =point-max= whenever a command is submitted so the cursor follows streaming output down to the new prompt. This is the interactive sibling of the queued-request submit fix above; the programmatic =:submit t= path was already covered
- Fix queue-compose =C-c C-c= leaving the shell cursor at its stale pre-queue position: the compose-buffer submit path is =:no-focus t=, so the shell window is never selected and the earlier submit fixes — which moved only buffer point via =goto-char= — left the shell window's =window-point= where the user had parked it before invoking =agent-shell-queue-request=. =agent-shell--handle= now also pushes =window-point= to =point-max= for every window showing the shell, so the visible cursor tracks the prompt even when the shell window is unselected (mirrors =agent-shell-jump-to-latest-permission-button-row=)

-----

Expand Down
144 changes: 134 additions & 10 deletions agent-shell.el
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,27 @@ Flow:
(shell-maker--current-request-id))
(map-put! (agent-shell--state) :last-activity-time (current-time))
(agent-shell--reset-insert-cursor)
;; A freshly submitted prompt should track the streaming response down
;; to the new prompt. Interactive `shell-maker-submit' (RET) leaves
;; point at the response boundary just below the echoed input — not at
;; `point-max' — because `shell-maker-point-at-last-prompt-p' is true,
;; so it skips its own `goto-char'. Streaming then inserts the reply
;; below point and the cursor is stranded above it for the whole turn
;; (shell-maker only auto-scrolls while point is at end-of-buffer).
;; Snap to `point-max' on submit so the cursor follows output.
;;
;; `goto-char' only moves BUFFER point. On the `:no-focus t'
;; queue-compose submit path (C-c C-c in the compose buffer) the
;; shell window is never selected, so its `window-point' keeps the
;; pre-queue position the user had parked point at and the cursor
;; visibly snaps back there. Push `window-point' to `point-max' for
;; every window showing the shell so the visible cursor tracks the
;; prompt too — mirrors
;; `agent-shell-jump-to-latest-permission-button-row'.
(when command
(goto-char (point-max))
(dolist (window (get-buffer-window-list nil nil t))
(set-window-point window (point-max))))
(cond ((not (map-elt (agent-shell--state) :client))
;; Needs a client
(agent-shell--emit-event :event 'init-started)
Expand Down Expand Up @@ -6224,13 +6245,20 @@ Returns an alist with insertion details or nil otherwise:
(let ((markdown-overlays-highlight-blocks agent-shell-highlight-blocks)
(markdown-overlays-render-images nil))
(markdown-overlays-put))))
;; Leave point at the start of the inserted region so the
;; user lands on their context, not after it — DWIM users
;; expect to keep typing where the prompt is, not below
;; the freshly-inserted text.
(goto-char insert-start)
(when submit
(shell-maker-submit)))
;; For a plain insertion, leave point at the start of the
;; inserted region so the user lands on their context, not
;; after it — DWIM users expect to keep typing where the
;; prompt is, not below the freshly-inserted text. When
;; submitting, that text becomes transcript above the new
;; prompt; leave point at `point-max' so it tracks the
;; prompt. shell-maker only auto-scrolls streamed output
;; while point is at end-of-buffer, so parking it above the
;; prompt would strand the cursor there for the whole turn.
(if submit
(progn
(goto-char (point-max))
(shell-maker-submit))
(goto-char insert-start)))
`((:buffer . ,shell-buffer)
(:start . ,insert-start)
(:end . ,insert-end)))
Expand Down Expand Up @@ -7068,6 +7096,7 @@ Optionally, get notified of completion with ON-SUCCESS function."
("!" "Shell command" agent-shell-insert-shell-command-output :transient t)
("@" "File" agent-shell-insert-file :transient t)
("d" "Dwim" agent-shell-send-dwim :transient t)
("q" "Quote last reply" agent-shell-quote-reply :transient t)
]]
[["Session"
("m" "Cycle modes" agent-shell-cycle-session-mode :transient t)
Expand Down Expand Up @@ -7335,6 +7364,76 @@ Remove: M-x agent-shell-remove-pending-request
(append pending (list prompt)))
(message "Request queued (%d pending)" (length (map-elt agent-shell--state :pending-requests)))))

(defun agent-shell--gfm-blockquote (text)
"Return TEXT wrapped as a GFM block quote.

Each line is prefixed with `> ' via `markdown-blockquote-region',
which also normalizes surrounding blank lines."
(with-temp-buffer
(insert text)
(markdown-blockquote-region (point-min) (point-max))
(string-trim (buffer-string))))

(defun agent-shell--last-agent-message-text ()
"Return the most recent agent prose reply as raw text, or nil.

Walks the current buffer backward from `point-max' for the latest
fragment whose `agent-shell-ui-state' qualified-id ends in
`-agent_message_chunk', then pulls the raw body from
`agent-shell-ui--content-store'. Tool-call drawers and thought
blocks are skipped.

`text-property-search-backward' returns nil when its predicate
rejects the nearest match instead of continuing the walk, so loop
manually — visiting each propertied region and checking the
qualified-id ourselves."
(when-let ((content-store agent-shell-ui--content-store))
(save-mark-and-excursion
(goto-char (point-max))
(let (qualified-id)
(while (and (not qualified-id)
(text-property-search-backward
'agent-shell-ui-state nil nil t))
(when-let* ((state (get-text-property (point) 'agent-shell-ui-state))
(qid (map-elt state :qualified-id))
((string-suffix-p "-agent_message_chunk" qid)))
(setq qualified-id qid)))
(when qualified-id
(gethash (concat qualified-id "-body") content-store))))))

(defun agent-shell--compose-initial-content (shell-buffer)
"Return initial content for a compose buffer popped from `current-buffer'.

For an active region in a buffer with no file association, block-quote
the raw region content. Otherwise delegate to `agent-shell--context',
which already produces purpose-built decoration for file-visiting
buffers, dired marked files, errors, etc. SHELL-BUFFER scopes
relative paths to the target shell's project."
(cond
((and (region-active-p) (not (buffer-file-name)))
(when-let* ((region (agent-shell--get-region :deactivate t))
(content (map-elt region :content)))
(agent-shell--gfm-blockquote content)))
(t
(agent-shell--context :shell-buffer shell-buffer))))

(defun agent-shell-quote-reply ()
"Pop the compose buffer prefilled with the agent's last reply, quoted.

Locates the most recent agent_message_chunk fragment in the current
shell buffer, wraps its body as a GFM block quote, and pops the
queue-compose buffer with that as initial content."
(declare (modes agent-shell-mode))
(interactive)
(unless (derived-mode-p 'agent-shell-mode)
(user-error "Not in a shell"))
(let ((text (agent-shell--last-agent-message-text)))
(unless text
(user-error "No agent reply found in this shell"))
(agent-shell-queue-compose-pop
(current-buffer)
:initial-content (agent-shell--gfm-blockquote text))))

(defun agent-shell-queue-request (&optional prompt)
"Queue or immediately send a request depending on shell busy state.

Expand All @@ -7344,6 +7443,13 @@ If the shell is busy when submitted, add to the pending requests
queue; otherwise submit immediately. Queued requests will be
automatically sent when the current request completes.

When called interactively, any DWIM context available in the current
buffer (active region, error at point, dired marked files, etc.) is
gathered via `agent-shell--context' and prefilled into the compose
buffer. An active region in a buffer with no file association is
wrapped as a GFM block quote; other context sources keep their
purpose-built formatting.

When called non-interactively with PROMPT, submit or queue
PROMPT directly, bypassing the compose buffer."
(declare (modes agent-shell-mode))
Expand All @@ -7352,7 +7458,10 @@ PROMPT directly, bypassing the compose buffer."
(user-error "Not in a shell"))
(cond
((not prompt)
(agent-shell-queue-compose-pop (current-buffer)))
(let ((shell-buffer (current-buffer)))
(agent-shell-queue-compose-pop
shell-buffer
:initial-content (agent-shell--compose-initial-content shell-buffer))))
((string-empty-p (string-trim prompt))
(user-error "PROMPT is empty"))
(t
Expand Down Expand Up @@ -7447,11 +7556,15 @@ idle."
(agent-shell--insert-to-shell-buffer
:shell-buffer shell-buffer :text prompt :submit t :no-focus t))))

(defun agent-shell-queue-compose-pop (shell-buffer)
(cl-defun agent-shell-queue-compose-pop (shell-buffer &key initial-content)
"Pop a `gfm-mode' compose buffer bound to SHELL-BUFFER.

Reuses the shell's existing compose buffer when alive so an
in-progress draft survives a re-invocation."
in-progress draft survives a re-invocation.

INITIAL-CONTENT, when supplied, is inserted at `point-max'. If the
buffer already has draft content, INITIAL-CONTENT is appended after
a blank-line separator."
(unless (buffer-live-p shell-buffer)
(user-error "Shell buffer is not live"))
(let* ((existing (buffer-local-value 'agent-shell--queue-compose-buffer
Expand Down Expand Up @@ -7482,6 +7595,17 @@ in-progress draft survives a re-invocation."
(with-current-buffer shell-buffer
(setq agent-shell--queue-compose-buffer new))
new))))
(when initial-content
(with-current-buffer buffer
(goto-char (point-max))
;; Ensure a blank-line separator before appending to an
;; existing draft so the new content doesn't run into it.
(unless (zerop (buffer-size))
(unless (bolp) (insert "\n"))
(unless (save-excursion (forward-line -1) (looking-at-p "^$"))
(insert "\n")))
(insert initial-content)
(set-buffer-modified-p nil)))
(pop-to-buffer buffer)
buffer))

Expand Down
Loading
Loading