fix(chat): make the run-failure card's retry tell the truth about the send gate (#7927)
* fix(chat): make the run-failure card's retry tell the truth about the send gate Three reports that all land on the same gate in ProjectView: `currentConversationActionDisabled`, which every recovery handler consults and no button was ever wired to. OPEND-2821 — the retry button had no `disabled` at all, so it always looked live. Clicking it reported a `manual_retry` recovery click and then `handleRetry` returned. Split that boolean into a four-way reason (read-only / messages-unavailable / billing-unresolved / conversation-busy), disable the card's recovery actions when it is set, and say which one it is. The split is a partition of the same predicate, proven exhaustively. OPEND-2758 — `handleSend` paints the new turn before the preflight and before POST /api/runs (OPEND-2614, deliberate), and that paint makes `retryableAssistantMessage` return null, so the card vanished 1-2s before anything was confirmed. Announce the pending retry, pin the card on the message being retried, lock the button and label it "Retrying", and drop the announcement only when the replacement message gets its run id -- or when the send never started, which restores the original card and its actions. OPEND-2719 — an exhausted wallet parked the send in the pending-send queue. The queue means "this runs once capacity frees up"; an empty wallet never frees up, so the message sat there forever. Stop queueing that one branch (`signed_out` keeps its queue-and-resume path) and hand the draft back to the composer through the existing `restore-draft` outcome. The report's other half did not reproduce: the dialog and balance card were already re-firing on every blocked send. Note on telemetry: a blocked click no longer counts as a recovery click. It never produced a run to join against, so those were unjoinable orphans; the existing surface_view still records that the action was offered. * fix(chat): claim the composer draft positively instead of by exclusion Review on #7927: the no-queue path for an exhausted wallet used an exclusion predicate — "not a retry and not a queue drain, therefore the composer owns it". ProjectView has eleven other direct `handleSend` callers (share to community, design-system feedback and review decisions, continue remaining tasks, resume run, question-form answers, Home auto-send, resend-failed-user-message, brand extraction and enrichment, the auto-audit repair effect, Antigravity OAuth repair). None of them holds its payload in the composer, and all of them were being swept onto that path — which silently took away their queue. The question-form answer is the worst case: it keeps the only copy of the answer and its uploads, and releases the form only on a durable-queue acknowledgement. Replace it with an explicit `composerOwnedDraft` marker that only `handleComposerSend` sets. Everything else falls through to the queueing path it had before this branch, unchanged. The marker is transport-only and is stripped alongside `acceptDurableQueue` before queue persistence: once a send is parked, the queue item owns the payload, not the composer.
L
lefarcen committed
9f73dcebf3906bc7eae571e65fd2ecfd7024cdcc
Parent: 92bf0b7
Committed by GitHub <noreply@github.com>
on 9/9/2026, 7:53:02 AM