diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 58b78d14..711c5c27 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -81,6 +81,7 @@ import { Button } from "@/components/ui/button" import { Toaster } from "@/components/ui/sonner" import { UpdateCard } from "@/components/update-card" import { BillingErrorDialog } from "@/components/billing-error-dialog" +import { BillingErrorNotice } from "@/components/billing-error-notice" import { CreditCelebration } from "@/components/credit-celebration" import { matchBillingError, type BillingErrorMatch } from "@/lib/billing-error" import { dispatchCreditExhausted, dispatchCreditReplenished } from "@/lib/credit-status" @@ -954,30 +955,34 @@ function App() { const [conversation, setConversation] = useState([]) const [billingErrorMatch, setBillingErrorMatch] = useState(null) const [billingErrorOpen, setBillingErrorOpen] = useState(false) - const lastHandledBillingErrorIdRef = useRef(null) + const handledBillingErrorIdsRef = useRef>(new Set()) const [currentAssistantMessage, setCurrentAssistantMessage] = useState('') - - useEffect(() => { - for (let i = conversation.length - 1; i >= 0; i--) { - const item = conversation[i] - if (!isErrorMessage(item)) continue - if (item.id === lastHandledBillingErrorIdRef.current) return - const match = matchBillingError(item.message) - if (match) { - lastHandledBillingErrorIdRef.current = item.id - setBillingErrorMatch(match) - setBillingErrorOpen(true) - if (match.kind === 'out_of_credits') dispatchCreditExhausted() - } - return - } - }, [conversation]) const [, setModelUsage] = useState(null) const [runId, setRunId] = useState(null) // New runtime: the active session's chat data + actions. All logic lives in // SessionChatStore (tested headlessly); the hook is a thin subscription. // runId IS the session id in the sessions runtime. const sessionChat = useSessionChat(runId) + + // Watch the conversation that is actually rendered — the sessions-runtime + // one when loaded, the legacy state otherwise — so billing failures + // (out of credits, subscription lapsed) always pop the upgrade dialog. + const billingWatchedConversation = sessionChat.chatState?.conversation ?? conversation + useEffect(() => { + for (let i = billingWatchedConversation.length - 1; i >= 0; i--) { + const item = billingWatchedConversation[i] + if (!isErrorMessage(item)) continue + if (handledBillingErrorIdsRef.current.has(item.id)) return + const match = matchBillingError(item.message) + if (match) { + handledBillingErrorIdsRef.current.add(item.id) + setBillingErrorMatch(match) + setBillingErrorOpen(true) + if (match.kind === 'out_of_credits') dispatchCreditExhausted() + } + return + } + }, [billingWatchedConversation]) const runIdRef = useRef(null) const loadRunRequestIdRef = useRef(0) const [isProcessing, setIsProcessing] = useState(false) @@ -6167,8 +6172,9 @@ function App() { } if (isErrorMessage(item)) { - if (matchBillingError(item.message)) { - return null + const billingMatch = matchBillingError(item.message) + if (billingMatch) { + return } return ( diff --git a/apps/x/apps/renderer/src/components/billing-error-notice.tsx b/apps/x/apps/renderer/src/components/billing-error-notice.tsx new file mode 100644 index 00000000..df871edb --- /dev/null +++ b/apps/x/apps/renderer/src/components/billing-error-notice.tsx @@ -0,0 +1,19 @@ +import { Message, MessageContent } from '@/components/ai-elements/message' +import type { BillingErrorMatch } from '@/lib/billing-error' + +/** + * Inline transcript notice for billing failures (out of credits, subscription + * required/inactive). The BillingErrorDialog is the attention-grabber; this + * stays in the conversation so the turn never ends silently blank if the + * dialog is dismissed or missed. + */ +export function BillingErrorNotice({ id, match }: { id: string; match: BillingErrorMatch }) { + return ( + + +

{match.title}

+

{match.subtitle}

+
+
+ ) +} diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index b3d5850c..0a7a327b 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -60,6 +60,7 @@ import { toToolState, } from '@/lib/chat-conversation' import { matchBillingError } from '@/lib/billing-error' +import { BillingErrorNotice } from '@/components/billing-error-notice' import { TokenUsageMenu } from '@/components/token-usage-menu' const streamdownComponents = { pre: MarkdownPreOverride } @@ -510,8 +511,9 @@ export function ChatSidebar({ } if (isErrorMessage(item)) { - if (matchBillingError(item.message)) { - return null + const billingMatch = matchBillingError(item.message) + if (billingMatch) { + return } return (