* Surface + recover from a waiting-room gate rejection. The server rejected * the request because our seat is no longer valid; update local state so the * UI reflects reality and we stop sending requests until we re-admit.
( kind: ReturnType<typeof getFreebuffGateErrorKind>, updater: BatchedMessageUpdater, )
| 546 | * UI reflects reality and we stop sending requests until we re-admit. |
| 547 | */ |
| 548 | function handleFreebuffGateError( |
| 549 | kind: ReturnType<typeof getFreebuffGateErrorKind>, |
| 550 | updater: BatchedMessageUpdater, |
| 551 | ) { |
| 552 | switch (kind) { |
| 553 | case 'session_expired': |
| 554 | case 'waiting_room_required': |
| 555 | case 'session_model_mismatch': |
| 556 | // Our seat is gone mid-chat. Finalize the AI message so its streaming |
| 557 | // indicator stops — otherwise `isComplete` stays false and the message |
| 558 | // keeps rendering a blinking cursor forever, making the user think the |
| 559 | // agent is still working even though the SessionEndedBanner is visible |
| 560 | // and actionable. Also disposes the batched-updater flush interval. |
| 561 | updater.markComplete() |
| 562 | // Flip to `ended` instead of auto re-queuing: the Chat surface stays |
| 563 | // mounted so any in-flight agent work can finish under the server-side |
| 564 | // grace period, and the session-ended banner prompts the user to press |
| 565 | // Enter when they're ready to rejoin. |
| 566 | markFreebuffSessionEnded() |
| 567 | return |
| 568 | case 'waiting_room_queued': |
| 569 | updater.setError( |
| 570 | "You're still in the waiting room. Please wait for admission before sending messages.", |
| 571 | ) |
| 572 | // Re-sync without resetting chat — this is a "we'll wait", not a |
| 573 | // "let's start fresh". |
| 574 | refreshFreebuffSession().catch(() => {}) |
| 575 | return |
| 576 | case 'session_superseded': |
| 577 | updater.setError( |
| 578 | 'Another freebuff CLI took over this account. Close the other instance, then restart.', |
| 579 | ) |
| 580 | // Terminal state: stop polling and flip UI to a "please restart" screen |
| 581 | // so we don't silently fight the other instance for the seat. |
| 582 | markFreebuffSessionSuperseded() |
| 583 | return |
| 584 | default: |
| 585 | return |
| 586 | } |
| 587 | } |
no test coverage detected