Skip to content

Restore point when no prompt is found - #43

Open
OSadovy wants to merge 1 commit into
xenodium:mainfrom
OSadovy:restore-point-when-no-prompt-found
Open

Restore point when no prompt is found#43
OSadovy wants to merge 1 commit into
xenodium:mainfrom
OSadovy:restore-point-when-no-prompt-found

Conversation

@OSadovy

@OSadovy OSadovy commented Aug 23, 2026

Copy link
Copy Markdown

The problem

I use agent-shell. Claude was in plan mode: it wrote a long plan into the shell
buffer and stopped for approval. The plan was plainly there in the shell buffer,
but agent-shell's viewport - which re-derives the current interaction through
shell-maker--command-and-response-at-point - showed the response cut off long
before the plan, ending part-way through an earlier tool call.

Earlier in that session the agent had read agent-shell's own source into the
transcript, so the buffer contained the literal text :shell-prompt "Claude> ".
That is what triggered it, but the lookalike text isn't the bug:
shell-maker--re-search-forward-prompt correctly rejected all four occurrences.

Cause

shell-maker--re-search-forward-prompt loops re-search-forward, moving point
onto each candidate and rejecting those without the comint-highlight-prompt
face. When it runs out of candidates it returns nil - but point is left on the
last rejected match instead of where it started.

shell-maker-narrow-to-prompt discards the return value and reads point instead:

(goto-char (shell-maker--prompt-end-position))
(shell-maker--re-search-forward-prompt ...)
(if (= begin (shell-maker--prompt-begin-position))
    (point-max)
  (shell-maker--prompt-begin-position))

"Did I move?" is a reasonable way to ask whether there was a next prompt, but it
only holds if a failed search leaves point alone. In my buffer point had been
dragged ~240k characters forward, so --prompt-begin-position answered with a
lookalike and narrowing stopped there. Everything past it - the entire plan -
fell outside the interaction.

The trailing prompt is why this isn't seen more often: after a finished exchange
there is a real prompt at the end, the search finds it, and nothing leaks. It
only goes wrong while the shell is still busy - which is exactly when a plan is
on screen waiting to be answered.

Fix

Restore point when nothing is found, the way re-search-forward itself does.

The other two callers, shell-maker-history-position and
shell-maker--extract-history, run inside their own save-excursion and don't
read point after a nil return, so they are unaffected.

Reproducing

Anchored regexp, no agent-shell, no real session data:

(require 'shell-maker)

(let* ((config (make-shell-maker-config
                :name "Repro"
                :prompt "Agent> "
                :prompt-regexp "^Agent> "
                ;; Write output but never finish, so no new prompt is
                ;; printed: the shell is still working.
                :execute-command
                (lambda (_command shell)
                  (funcall (map-elt shell :write-output)
                           (concat "Here is a transcript excerpt:\n"
                                   "Agent> hello\n"
                                   "and THE-TAIL of the answer\n"))))))
  (shell-maker-start-v2 :config config :no-focus t)
  (with-current-buffer (shell-maker-buffer config)
    (goto-char (point-max))
    (insert "explain")
    (shell-maker-submit)
    (goto-char comint-last-input-start)
    (princ (format "%S\n" (cdr (shell-maker--command-and-response-at-point))))))

Before: "Here is a transcript excerpt:" - the response stops at the lookalike.
After: the full three lines.

Note the regexp is anchored, so this is not about unanchored prompt regexps.
(agent-shell does pass unanchored ones)

Test

shell-maker-history-test-search-prompt-leaves-point-when-not-found, added next
to the existing -extract-preserves-point. Fails before the change, passes
after, and the other 14 in that file still pass.

What else I considered

shell-maker--prompt-begin-position searches backward with a plain
re-search-backward comint-prompt-regexp and no face check, so it can still
anchor on a lookalike sitting between the real prompt and point. Guarding it
would independently have saved me here, but it decides where interactions begin
for shell-maker-delete-interaction-at-point too, and it needs a "found
nothing" case designed. I'd rather get package author's call on it.

shell-maker--re-search-forward-prompt walks past prompt lookalikes in
response text, rejecting those without the comint-highlight-prompt
face.  When none of them is a real prompt it returns nil, but leaves
point on the last one searched rather than where it started.

shell-maker-narrow-to-prompt discards the return value and instead
asks whether point moved, so a leaked point makes it end the current
interaction at a lookalike.  Everything after that falls outside, and
callers such as shell-maker--command-and-response-at-point see a
truncated response.  This only bites while a shell is still busy: once
a new prompt is printed the search finds it and point never leaks.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant