({
isStreaming,
})
| 29 | * once no in-flight work needs the global stream-interrupt handler. |
| 30 | */ |
| 31 | export const SessionEndedBanner: React.FC<SessionEndedBannerProps> = ({ |
| 32 | isStreaming, |
| 33 | }) => { |
| 34 | const theme = useTheme() |
| 35 | const [pendingAction, setPendingAction] = useState< |
| 36 | 'waiting-room' | 'same-chat' | null |
| 37 | >(null) |
| 38 | |
| 39 | // All premium models share one daily pool; the server replicates the same |
| 40 | // snapshot under each premium model id, so the first entry has the right |
| 41 | // count. |
| 42 | const premiumQuota = useFreebuffSessionStore( |
| 43 | (s) => Object.values(getRateLimitsByModel(s.session) ?? {})[0] ?? null, |
| 44 | ) |
| 45 | const isQuotaExhausted = premiumQuota |
| 46 | ? premiumQuota.recentCount >= premiumQuota.limit |
| 47 | : false |
| 48 | const accessTier = useFreebuffSessionStore((s) => |
| 49 | s.session && 'accessTier' in s.session ? s.session.accessTier : 'full', |
| 50 | ) |
| 51 | const quotaLabel = |
| 52 | accessTier === 'limited' ? 'sessions' : 'premium sessions' |
| 53 | const bannerTitle = premiumQuota |
| 54 | ? `Session ended · ${formatSessionUnits(premiumQuota.recentCount)} of ${premiumQuota.limit} ${quotaLabel} used today` |
| 55 | : 'Session ended' |
| 56 | const landingButtonLabel = |
| 57 | accessTier === 'limited' ? 'Back to start' : 'Change model' |
| 58 | const landingPendingLabel = |
| 59 | accessTier === 'limited' |
| 60 | ? 'Opening start screen…' |
| 61 | : 'Opening model selection…' |
| 62 | |
| 63 | // While a request is still streaming, restart is disabled: it would |
| 64 | // unmount <Chat> and abort the in-flight agent run. The promise is "we |
| 65 | // let the agent finish" — honoring that means Enter does nothing until |
| 66 | // the stream ends or the user hits Esc. |
| 67 | const canRestart = !isStreaming && pendingAction === null |
| 68 | const pickNewModel = useCallback(() => { |
| 69 | if (!canRestart) return |
| 70 | setPendingAction('waiting-room') |
| 71 | // Drop back to the landing picker (status: 'none') so the user picks a |
| 72 | // model and hits Enter again to commit, instead of being silently |
| 73 | // re-queued. app.tsx swaps us into <WaitingRoomScreen> on the |
| 74 | // transition, unmounting this banner — no need to clear the pending state on |
| 75 | // success. |
| 76 | returnToFreebuffLanding({ resetChat: true }).catch(() => |
| 77 | setPendingAction(null), |
| 78 | ) |
| 79 | }, [canRestart]) |
| 80 | |
| 81 | const startSameChatSession = useCallback(() => { |
| 82 | if (!canRestart) return |
| 83 | setPendingAction('same-chat') |
| 84 | // Re-POST with the currently selected model and keep the chat/run state |
| 85 | // intact so the next prompt continues the same conversation. |
| 86 | refreshFreebuffSession().catch(() => setPendingAction(null)) |
| 87 | }, [canRestart]) |
| 88 |
nothing calls this directly
no test coverage detected