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
1 change: 1 addition & 0 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ 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]])
- Fix streaming cursor drop: a streamed message fragment inserted at the prompt no longer strands point above the inserted text. The shell render path relied on shell-maker's insertion-type nil =save-excursion=, which stays before text inserted at point, so when the cursor sat at an empty prompt (or the start of pending input) with auto-scroll suppressed it was left behind the freshly streamed block. A new =agent-shell--with-preserved-point= saves point in an insertion-type t marker (mirroring the tool-output path) and snaps to =point-max= when point was at end-of-buffer, so the cursor always tracks the prompt

-----

Expand Down
35 changes: 31 additions & 4 deletions agent-shell.el
Original file line number Diff line number Diff line change
Expand Up @@ -3050,6 +3050,31 @@ live process-mark is pushed forward by the insertion."
(marker-position (process-mark ,proc-sym)))
(set-marker ,saved-sym nil))))))

(defmacro agent-shell--with-preserved-point (&rest body)
"Evaluate BODY, then restore point so it stays with the prompt.
Fragment updates insert text at the insert-cursor, which can sit
exactly at point when the prompt is empty or point is at the start
of the input area. shell-maker's `save-excursion'-based point
preservation uses an insertion-type nil marker, which stays BEFORE
text inserted at point, stranding point above the freshly inserted
fragment (the cursor appears to \"drop behind\" streamed text).

Save point in a marker with insertion-type t so it advances past
insertions made at point, and snap to `point-max' when point was at
end-of-buffer — the prompt — so a streaming chunk never leaves the
cursor behind regardless of shell-maker's auto-scroll decision."
(declare (indent 0) (debug body))
(let ((was-at-end-sym (make-symbol "was-at-end"))
(saved-sym (make-symbol "saved-point")))
`(let ((,was-at-end-sym (eobp))
(,saved-sym (copy-marker (point) t)))
(unwind-protect
(progn ,@body)
(if ,was-at-end-sym
(goto-char (point-max))
(goto-char ,saved-sym))
(set-marker ,saved-sym nil)))))

(defun agent-shell--insert-cursor ()
"Return the insertion cursor for the current shell buffer.
The cursor is a marker with insertion-type t that advances past
Expand Down Expand Up @@ -3249,7 +3274,8 @@ by default, RENDER-BODY-IMAGES to enable inline image rendering in body."
(equal (current-buffer)
(map-elt state :buffer)))
(error "Editing the wrong buffer: %s" (current-buffer)))
(agent-shell--with-preserved-process-mark
(agent-shell--with-preserved-point
(agent-shell--with-preserved-process-mark
(shell-maker-with-auto-scroll-edit
(when-let* ((range (agent-shell-ui-update-fragment
(agent-shell-ui-make-fragment-model
Expand Down Expand Up @@ -3290,7 +3316,7 @@ by default, RENDER-BODY-IMAGES to enable inline image rendering in body."
(append "append")
(t "rebuild"))
(or namespace-id (map-elt state :request-count))
block-id append))))))
block-id append)))))))

(cl-defun agent-shell--update-text (&key state namespace-id block-id text append create-new)
"Update plain text entry in the shell buffer.
Expand All @@ -3316,7 +3342,8 @@ APPEND and CREATE-NEW control update behavior."
:create-new create-new
:no-undo t))))
(with-current-buffer (map-elt state :buffer)
(agent-shell--with-preserved-process-mark
(agent-shell--with-preserved-point
(agent-shell--with-preserved-process-mark
(shell-maker-with-auto-scroll-edit
(when-let* ((range (agent-shell-ui-update-text
:namespace-id ns
Expand All @@ -3334,7 +3361,7 @@ APPEND and CREATE-NEW control update behavior."
(cond (create-new "create")
(append "append")
(t "rebuild"))
ns block-id append)))))))
ns block-id append))))))))

(defun agent-shell-toggle-logging ()
"Toggle logging."
Expand Down
51 changes: 51 additions & 0 deletions tests/agent-shell-streaming-tests.el
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,57 @@ must appear before the prompt text, not after it."
(should (string-suffix-p "hello\n\n"
(buffer-substring-no-properties (point-min) (point-max)))))))

;;; Point preservation across streaming insertions (cursor drop regression)

(ert-deftest agent-shell--with-preserved-point-keeps-cursor-at-empty-prompt-test ()
"A streaming insertion at point must not strand the cursor behind it.
Regression: when point sits at the insert-cursor position (an empty
prompt at end-of-buffer) and shell-maker's non-auto-scroll branch
preserves point with an insertion-type nil `save-excursion' marker,
text inserted at point leaves that marker — and thus point — BEFORE
the inserted text, so the cursor appears to drop behind streamed
output. `agent-shell--with-preserved-point' must snap point back to
the prompt at `point-max'."
(with-temp-buffer
(insert "previous output\n\nClaude Code> ")
;; Point is at the (empty) prompt = where a streaming fragment is
;; inserted = end-of-buffer.
(let ((insert-pos (point)))
(agent-shell--with-preserved-point
;; Mimic shell-maker-with-auto-scroll-edit's non-auto-scroll
;; branch: an insertion-type nil save-excursion around an
;; insertion made at the insert-cursor (= point).
(save-excursion
(goto-char insert-pos)
(insert "STREAMED")))
;; Without the fix point would be stranded at INSERT-POS, behind
;; "STREAMED"; it must instead stay with the prompt at point-max.
(should (= (point) (point-max)))
(should (string-suffix-p "STREAMED"
(buffer-substring-no-properties (point-min) (point-max)))))))

(ert-deftest agent-shell--with-preserved-point-advances-past-insertion-test ()
"Point not at end-of-buffer must advance past text inserted at point.
When the user has moved point to the start of their pending input (the
insert-cursor position) but is not at end-of-buffer, a streamed
fragment inserted there must push point forward with the input rather
than leaving it stranded above the inserted text."
(with-temp-buffer
(insert "previous output\n\nClaude Code> hello")
;; Move point to the start of the input, the insert-cursor position,
;; but NOT end-of-buffer.
(goto-char (- (point) (length "hello")))
(let ((insert-pos (point)))
(agent-shell--with-preserved-point
(save-excursion
(goto-char insert-pos)
(insert "STREAMED")))
;; Point must sit just after the inserted text, still at the start
;; of "hello" — not stranded above "STREAMED".
(should (looking-at-p "hello"))
(should (string= (buffer-substring-no-properties (point) (point-max))
"hello")))))

(ert-deftest agent-shell--mark-tool-calls-cancelled-with-nil-transcript-test ()
"Cancelling in-flight tool calls must not signal when transcript-file is nil.
agent-shell--mark-tool-calls-cancelled invokes handle-tool-call-final
Expand Down
Loading