From 43d1559bb70b2ed6b170caab7596d5b60d02a4aa Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 19 Apr 2026 16:21:07 -0700 Subject: [PATCH] Fix freebuff grace-period hang where UI looks stuck streaming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate hang vectors after a freebuff session ends mid-run: 1. `handleFreebuffGateError` for session_expired/waiting_room_required only flipped session state — it never finalized the in-flight AI message. Result: `isComplete` stayed false, the streaming cursor kept rendering, and the batched-updater flush interval leaked. Users saw an apparently still-streaming message next to the rejoin banner, assumed the agent was working, and waited indefinitely. Now calls `updater.markComplete()` so the message visibly finalizes. 2. If the agent was mid-`ask_user` when the session ended, the SessionEndedBanner replaced the ChatInputBar and hid the answer form, leaving the agent waiting on input that could never arrive. Chat.tsx now lets ask_user take precedence over the banner (same pattern as review mode) so in-flight runs can still finish during grace. Co-Authored-By: Claude Opus 4.7 --- cli/src/chat.tsx | 10 ++++++---- cli/src/hooks/helpers/send-message.ts | 14 ++++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cli/src/chat.tsx b/cli/src/chat.tsx index bafdcecf1..af83a45c9 100644 --- a/cli/src/chat.tsx +++ b/cli/src/chat.tsx @@ -1473,15 +1473,17 @@ export const Chat = ({ )} {reviewMode ? ( - // Review takes precedence over the session-ended banner: during the - // grace window the agent may still be asking to run tools, and - // those approvals must be reachable for the run to finish. + // Review and ask_user take precedence over the session-ended banner: + // during the grace window the agent may still be asking to run tools + // or asking the user a question, and those approvals/answers must be + // reachable for the run to finish — otherwise the agent hangs + // waiting for input that can never be given. - ) : isFreebuffSessionOver ? ( + ) : isFreebuffSessionOver && !askUserState ? ( diff --git a/cli/src/hooks/helpers/send-message.ts b/cli/src/hooks/helpers/send-message.ts index 01f6880b6..02e419b30 100644 --- a/cli/src/hooks/helpers/send-message.ts +++ b/cli/src/hooks/helpers/send-message.ts @@ -510,10 +510,16 @@ function handleFreebuffGateError( switch (kind) { case 'session_expired': case 'waiting_room_required': - // Our seat is gone mid-chat. Flip to `ended` instead of auto re-queuing: - // the Chat surface stays mounted so any in-flight agent work can finish - // under the server-side grace period, and the session-ended banner - // prompts the user to press Enter when they're ready to rejoin. + // Our seat is gone mid-chat. Finalize the AI message so its streaming + // indicator stops — otherwise `isComplete` stays false and the message + // keeps rendering a blinking cursor forever, making the user think the + // agent is still working even though the SessionEndedBanner is visible + // and actionable. Also disposes the batched-updater flush interval. + updater.markComplete() + // Flip to `ended` instead of auto re-queuing: the Chat surface stays + // mounted so any in-flight agent work can finish under the server-side + // grace period, and the session-ended banner prompts the user to press + // Enter when they're ready to rejoin. markFreebuffSessionEnded() return case 'waiting_room_queued':