diff --git a/agent-shell.el b/agent-shell.el index 074eaa6a..fd64f9f7 100644 --- a/agent-shell.el +++ b/agent-shell.el @@ -1529,6 +1529,19 @@ COMMAND, when present, may be a shell command string or an argv vector." ((null command) nil) (t (error "Unexpected tool-call command type: %S" (type-of command))))) +(defun agent-shell--should-render-update-p (state) + "Return non-nil if a session/update notification should render now. +True when this client has an in-flight ACP request awaiting response, or +when a session is established. The latter case covers shared sessions: +when another client also attaches to the same agent, the agent's +session/update notifications reach every attached frontend regardless +of which one originated the prompt, so this client may have no +`:active-requests' yet still receive renderable agent_message_chunks, +agent_thought_chunks, tool_calls, and tool_call_updates that should +appear in the buffer rather than being treated as stale." + (or (map-elt state :active-requests) + (map-nested-elt state '(:session :id)))) + (defun agent-shell--active-requests-p (state) "Return non-nil if STATE has in-flight requests awaiting responses." (map-elt state :active-requests)) @@ -1539,9 +1552,12 @@ COMMAND, when present, may be a shell command string or an argv vector." (cond ((equal (map-elt acp-notification 'method) "session/update") (cond ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "tool_call") - ;; Notification is out of context (session/prompt finished). - ;; Cannot derive where to display, so show in minibuffer. - (if (not (agent-shell--active-requests-p state)) + ;; Render unless truly out of context (no active request and no + ;; established session). When another client shares this + ;; session, its prompts produce tool_call updates here that + ;; are not tied to this client's :active-requests but still + ;; belong in the buffer. + (if (not (agent-shell--should-render-update-p state)) (when acp-logging-enabled (message "%s %s (stale, consider reporting to ACP agent)" (agent-shell--make-status-kind-label @@ -1593,9 +1609,10 @@ COMMAND, when present, may be a shell command string or an argv vector." :expanded t))) (map-put! state :last-entry-type "tool_call"))) ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "agent_thought_chunk") - ;; Notification is out of context (session/prompt finished). - ;; Cannot derive where to display, so show in minibuffer. - (if (not (agent-shell--active-requests-p state)) + ;; Render unless truly out of context (no active request and no + ;; established session). Prompts from another client sharing + ;; this session produce thought chunks that should still appear. + (if (not (agent-shell--should-render-update-p state)) (when acp-logging-enabled (message "%s %s (stale, consider reporting to ACP agent): %s" agent-shell-thought-process-icon @@ -1625,9 +1642,12 @@ COMMAND, when present, may be a shell command string or an argv vector." :expanded agent-shell-thought-process-expand-by-default) (map-put! state :last-entry-type "agent_thought_chunk"))) ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "agent_message_chunk") - ;; Notification is out of context (session/prompt finished). - ;; Cannot derive where to display, so show in minibuffer. - (if (not (agent-shell--active-requests-p state)) + ;; Render unless truly out of context (no active request and no + ;; established session). When another client shares this + ;; session, agent_message_chunks arrive in response to its + ;; prompts and should appear in the buffer rather than being + ;; routed to *Messages*. + (if (not (agent-shell--should-render-update-p state)) (when acp-logging-enabled (message "Agent message (stale, consider reporting to ACP agent): %s" (truncate-string-to-width (map-nested-elt acp-notification '(params update content text)) 100))) @@ -1656,14 +1676,17 @@ COMMAND, when present, may be a shell command string or an argv vector." :render-body-images t) (map-put! state :last-entry-type "agent_message_chunk"))) ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "user_message_chunk") - ;; Only handle user_message_chunks when there's an active session/load - ;; or session/push to avoid inserting a redundant shell prompt - ;; with the existing user submission. - (when (seq-find (lambda (r) - (member (map-elt r :method) - (append '("session/load") - (agent-shell-experimental--methods)))) - (map-elt state :active-requests)) + ;; Render whenever there's somewhere to put it — local session + ;; established or a history-replay request in flight. When + ;; another client shares this session via a proxy, the proxy + ;; is expected to exclude the originating frontend from any + ;; synthesized user_message_chunk broadcast, so an arrival + ;; here is from another client's typing rather than an echo + ;; of this client's own session/prompt — including when that + ;; own session/prompt is currently in flight. The previous + ;; guard (skip while own session/prompt was active) wrongly + ;; dropped those mid-turn arrivals. + (when (agent-shell--should-render-update-p state) (let ((new-prompt-p (not (equal (map-elt state :last-entry-type) "user_message_chunk"))) (content-text (or (map-nested-elt acp-notification '(params update content text)) @@ -1694,6 +1717,39 @@ COMMAND, when present, may be a shell command string or an argv vector." :create-new new-prompt-p :append t)) (map-put! state :last-entry-type "user_message_chunk"))) + ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "turn_complete") + ;; Synthesized by a session-sharing proxy when a session/prompt + ;; response arrives. Only the non-originating clients see this — + ;; the originator gets the response directly and finalizes via + ;; its :on-success callback. Without this arm, turns driven by + ;; another client leave the buffer with a read-only tail and no + ;; fresh prompt to type at. + (unless (seq-find (lambda (r) + (equal (map-elt r :method) "session/prompt")) + (map-elt state :active-requests)) + (let* ((stop-reason (map-nested-elt acp-notification '(params update stopReason))) + (success (equal stop-reason "end_turn"))) + (when (equal (map-elt state :last-entry-type) "agent_message_chunk") + (agent-shell--append-transcript + :text "\n\n" + :file-path agent-shell--transcript-file)) + (map-put! state :tool-calls nil) + (unless success + (agent-shell--update-fragment + :state state + :block-id (format "%s-stop-reason" + (map-elt state :request-count)) + :body (agent-shell--stop-reason-description stop-reason) + :create-new t)) + (when-let ((buf (map-elt state :buffer)) + ((buffer-live-p buf))) + (with-current-buffer buf + (shell-maker-finish-output :config shell-maker--config + :success t))) + (agent-shell--emit-event + :event 'turn-complete + :data (list (cons :stop-reason stop-reason))) + (map-put! state :last-entry-type nil)))) ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "plan") (agent-shell--update-fragment :state state @@ -1703,9 +1759,11 @@ COMMAND, when present, may be a shell command string or an argv vector." :expanded t) (map-put! state :last-entry-type "plan")) ((equal (map-nested-elt acp-notification '(params update sessionUpdate)) "tool_call_update") - ;; Notification is out of context (session/prompt finished). - ;; Cannot derive where to display, so show in minibuffer. - (if (not (agent-shell--active-requests-p state)) + ;; Render unless truly out of context (no active request and no + ;; established session). When another client shares this + ;; session, its prompts produce tool_call_update events that + ;; should refresh the existing card rather than being dropped. + (if (not (agent-shell--should-render-update-p state)) (when acp-logging-enabled (message "%s %s (stale, consider reporting to ACP agent)" (agent-shell--make-status-kind-label