Skip to content
Merged
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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Auto-dismiss a pending `ask_user` question when another client (e.g. an SSE/web client in remote mode, see eca 0.139.0) answers it first. The server resolves the `ask_user` tool out from under us and sends a `toolCalled`/`toolCallRejected` for that tool-call id but no longer expects our answer (it cancels our request); we now correlate that id with `eca-chat--pending-question` and clear the stale answer-mode prompt state instead of staying stuck waiting for input.
- Paginate long chats instead of replaying the full history on open. With `eca-chat-history-page-size` non-nil (default 50), `eca-chat-resume` opens a chat with only the newest page and shows a clickable "Load older messages" control at the top to page through earlier history on demand via the new `chat/history` request (also bound to `C-c C-S-o`). Older pages are prepended above the existing content, reusing the streaming renderer (subagent nesting included) and kept read-only like the rest of the history. Set `eca-chat-history-page-size` to nil to keep the previous full-replay behavior.
- Bugfix: resuming a previous session after a server restart no longer reopens it as a `*Closed session*` (mode-line) / `This chat is closed` buffer that rejects new prompts. `eca-chat-exit` (run on restart) marks the most-recent chat buffer `eca-chat--closed` but leaves it in the session registry; `eca-chat-opened` was treating that live-but-closed buffer as "already known" and replaying the resumed history into it. It now treats a registered buffer marked `eca-chat--closed` as stale, killing the leftover closed buffer and creating a fresh, writable one in its place.
- Color the Doom Emacs workspace tabline by ECA session status: orange when a chat waits for approval/question, dim yellow while running. Enabled automatically on Doom (`:ui workspaces` module), disable with `eca-doom-workspace-tabs`.
Expand Down
21 changes: 21 additions & 0 deletions eca-chat.el
Original file line number Diff line number Diff line change
Expand Up @@ -3264,6 +3264,9 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
(remhash id eca-chat--tool-call-prepare-content-cache)
;; Stop elapsed-time tracking for this tool call
(eca-chat--tool-call-elapsed-stop id)
;; Another client may have answered this ask_user question first,
;; resolving the tool call here; drop our now-stale prompt state.
(eca-chat--dismiss-pending-question-for-tool-call id)
;; Cleanup subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
Expand Down Expand Up @@ -3303,6 +3306,9 @@ Must be called with `eca-chat--with-current-buffer' or equivalent."
;; Cleanup counters for this tool-call id
(remhash id eca-chat--tool-call-prepare-counters)
(remhash id eca-chat--tool-call-prepare-content-cache)
;; Another client may have cancelled this ask_user question first,
;; rejecting the tool call here; drop our now-stale prompt state.
(eca-chat--dismiss-pending-question-for-tool-call id)
;; Cleanup subagent mapping only for top-level tool calls
(when (and (not parent-tool-call-id)
(string= "subagent" (plist-get details :type)))
Expand Down Expand Up @@ -3884,6 +3890,21 @@ Used as fallback when no toolCallId is available."
session request
(list :answer nil :cancelled t)))))

(defun eca-chat--dismiss-pending-question-for-tool-call (tool-call-id)
"Dismiss the pending question when it belongs to TOOL-CALL-ID.
Used when another client answers the same `ask_user' question first, so
the server resolves the tool call out from under us. The tool output is
rendered by the caller; here we only restore the local question state
and prompt so this client does not stay stuck in answer mode."
(when (and eca-chat--pending-question
(equal tool-call-id
(plist-get eca-chat--pending-question :tool-call-id)))
(let ((allow-freeform (plist-get eca-chat--pending-question :allow-freeform)))
(setq eca-chat--pending-question nil)
(when allow-freeform
(eca-chat--set-question-prompt-prefix nil))
(eca-chat--refresh-transient-area))))

(defun eca-chat--set-question-prompt-prefix (active)
"Toggle the prompt prefix for question mode.
When ACTIVE is non-nil, show the question prefix; otherwise restore normal."
Expand Down
38 changes: 38 additions & 0 deletions test/eca-chat-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,44 @@ does not treat the first line as metadata. Returns FN's value."
(expect 'eca-chat--set-chat-loading :to-have-been-called-with
session 'stopping)))))

(describe "eca-chat--dismiss-pending-question-for-tool-call"
;; When another client answers/cancels the same `ask_user' question first,
;; the server resolves the tool call and we receive a `toolCalled' /
;; `toolCallRejected' for that id. This client must then drop its now-stale
;; pending-question state so the prompt leaves answer mode.
(it "clears the pending question when the tool-call id matches"
(with-temp-buffer
(setq-local eca-chat--pending-question
(list :session (make-eca--session) :request 1
:tool-call-id "tc-1" :allow-freeform t))
(spy-on 'eca-chat--set-question-prompt-prefix)
(spy-on 'eca-chat--refresh-transient-area)
(eca-chat--dismiss-pending-question-for-tool-call "tc-1")
(expect eca-chat--pending-question :to-be nil)
(expect 'eca-chat--set-question-prompt-prefix
:to-have-been-called-with nil)
(expect 'eca-chat--refresh-transient-area :to-have-been-called)))

(it "leaves the pending question intact when the id does not match"
(with-temp-buffer
(let ((pending (list :session (make-eca--session) :request 1
:tool-call-id "tc-1" :allow-freeform t)))
(setq-local eca-chat--pending-question pending)
(spy-on 'eca-chat--set-question-prompt-prefix)
(spy-on 'eca-chat--refresh-transient-area)
(eca-chat--dismiss-pending-question-for-tool-call "tc-2")
(expect eca-chat--pending-question :to-equal pending)
(expect 'eca-chat--set-question-prompt-prefix
:not :to-have-been-called))))

(it "does nothing when there is no pending question"
(with-temp-buffer
(setq-local eca-chat--pending-question nil)
(spy-on 'eca-chat--refresh-transient-area)
(eca-chat--dismiss-pending-question-for-tool-call "tc-1")
(expect eca-chat--pending-question :to-be nil)
(expect 'eca-chat--refresh-transient-area :not :to-have-been-called))))

(describe "eca-chat--normalize-question-option"
;; Regression: a `chat/askQuestion' option that is a plain string or a
;; plist without `:label' must not crash rendering with
Expand Down
Loading