(function UserI
)
if (accepted === false) return
clearComposer()
- }, [stopSendWithoutChatPlan, onSubmit, clearComposer])
+ }, [chatPlanRequired, onSubmit, clearComposer])
/**
* Enter policy for the editor: mirror canSubmit's uploading guard (Enter
diff --git a/apps/sim/app/workspace/[workspaceId]/home/home.tsx b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
index 1e3523389f6..724943dfac0 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/home.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/home/home.tsx
@@ -41,6 +41,7 @@ import { useMarkMothershipChatRead } from '@/hooks/queries/mothership-chats'
import { useOAuthReturnRouter } from '@/hooks/use-oauth-return'
import type { ChatContext } from '@/stores/panel'
import {
+ ChatPlanNotice,
ChatSurfaceProvider,
CreditsChip,
MothershipChat,
@@ -442,6 +443,7 @@ function HomeContent({ chatId, userName, userId }: HomeProps) {
What should we get done{firstName ? `, ${firstName}` : ''}?
+
boolean {
- const hostContext = useOptionalWorkspaceHostContext()
- const { data: session } = useSession()
- const viewerId = session?.user?.id
- const router = useRouter()
-
- return useCallback(() => {
- if (!hostContext || hostContext.features?.chat !== false) return false
-
- if (canManageWorkspaceBilling(hostContext, viewerId)) {
- router.push(buildUpgradeHref(hostContext.workspace.id))
- } else {
- toast.error(
- hostContext.hostOrganizationId
- ? `${CHAT_PLAN_REQUIRED_MESSAGE} Contact an organization admin to upgrade this workspace’s plan.`
- : `${CHAT_PLAN_REQUIRED_MESSAGE} Ask the workspace owner to upgrade this workspace’s plan.`
- )
- }
- return true
- }, [hostContext, viewerId, router])
-}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat-plan-required.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat-plan-required.ts
new file mode 100644
index 00000000000..6202739c1be
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat-plan-required.ts
@@ -0,0 +1,15 @@
+'use client'
+
+import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
+
+/**
+ * Whether this workspace's payer may not use Chat, so the composer shows the upgrade card and
+ * holds sends that run admission would refuse.
+ *
+ * The verdict is the host context's `features.chat`, which the server resolves with the same
+ * check admission runs. A surface without a workspace host (an organization conversation), or a
+ * context that predates the flag, is not held here; admission still answers it with the card.
+ */
+export function useChatPlanRequired(): boolean {
+ return useOptionalWorkspaceHostContext()?.features?.chat === false
+}
diff --git a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
index d21ab236c1f..a3a5caffcee 100644
--- a/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
+++ b/apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
@@ -119,7 +119,7 @@ import {
dispatchStreamEvent,
finalizeResidualToolCalls,
} from '@/app/workspace/[workspaceId]/home/hooks/stream'
-import { useChatPlanGate } from '@/app/workspace/[workspaceId]/home/hooks/use-chat-plan-gate'
+import { useChatPlanRequired } from '@/app/workspace/[workspaceId]/home/hooks/use-chat-plan-required'
import { useNativeActiveTabIds } from '@/app/workspace/[workspaceId]/home/hooks/use-desktop-tab-resources'
import { useMothershipChatOlderMessages } from '@/app/workspace/[workspaceId]/home/hooks/use-mothership-chat-older-messages'
import { resolveEffectiveResourceId } from '@/app/workspace/[workspaceId]/home/resource-view-policy'
@@ -780,7 +780,7 @@ export function useChat(
const pathname = usePathname()
const router = useRouter()
const queryClient = useQueryClient()
- const stopSendWithoutChatPlan = useChatPlanGate()
+ const chatPlanRequired = useChatPlanRequired()
const [pendingMessages, setPendingMessages] = useState([])
const [completedTurn, setCompletedTurn] = useState(null)
const completedTurnRef = useRef(null)
@@ -3682,6 +3682,7 @@ export function useChat(
)
if ((!message.trim() && !fileAttachments?.length) || !scopeKey) return false
+ if (chatPlanRequired) return false
const { onOptimisticSendApplied } = options ?? {}
const pendingStop = options?.pendingStop ?? pendingStopPromiseRef.current
let queuedSendHandoff = options?.queuedSendHandoff
@@ -4392,6 +4393,7 @@ export function useChat(
workspaceId,
organizationId,
scopeKey,
+ chatPlanRequired,
queryClient,
upsertChatHistory,
modelSelectorEnabled,
@@ -4439,7 +4441,7 @@ export function useChat(
options?: SendMessageOptions
) => {
if ((!message.trim() && !fileAttachments?.length) || !scopeKey) return
- if (stopSendWithoutChatPlan()) return
+ if (chatPlanRequired) return
const queueStore = useMothershipQueueStore.getState()
const activeChatKey = chatKeyRef.current
@@ -4527,7 +4529,7 @@ export function useChat(
},
[
workspaceId,
- stopSendWithoutChatPlan,
+ chatPlanRequired,
createQueuedMessage,
startSendMessage,
handOffWithdrawnSend,
@@ -5300,6 +5302,7 @@ export function useChat(
const msg = id === undefined ? queue?.[0] : queue?.find((queued) => queued.id === id)
if (!msg || queueState.editing[chatKey] === msg.id) return
if (queuedMessageDispatchIds.has(msg.id)) return
+ if (chatPlanRequired) return
/* Sent by hand, it still must not run a second turn: if the history shows it
accepted, it is already in the chat. A history that cannot be read does not
hold it back here; the user asked to send it, and the server deduplicates it
@@ -5371,6 +5374,7 @@ export function useChat(
})
},
[
+ chatPlanRequired,
dispatchQueuedMessage,
queryClient,
stopGeneration,
@@ -5461,7 +5465,7 @@ export function useChat(
const [sendRetryWakeup, setSendRetryWakeup] = useState(0)
useEffect(() => {
if (!scopeKey) return
- if (messageQueue.length === 0 || queueHeadHeld) return
+ if (messageQueue.length === 0 || queueHeadHeld || chatPlanRequired) return
if (queueHeadNotBefore !== undefined && queueHeadNotBefore > Date.now()) {
const timer = setTimeout(
() => setSendRetryWakeup((wakeups) => wakeups + 1),
@@ -5480,6 +5484,7 @@ export function useChat(
scopeKey,
messageQueue.length,
queueHeadHeld,
+ chatPlanRequired,
queueHeadNotBefore,
sendRetryWakeup,
resolvedChatId,
diff --git a/apps/sim/lib/billing/chat-plan.ts b/apps/sim/lib/billing/chat-plan.ts
index a1153b6c037..1e50f9634b6 100644
--- a/apps/sim/lib/billing/chat-plan.ts
+++ b/apps/sim/lib/billing/chat-plan.ts
@@ -1,5 +1,13 @@
/** The upgrade card's `reason` for a payer whose plan does not include Chat. */
export const CHAT_PLAN_REQUIRED_REASON = 'plan_required' as const
-/** Lead sentence of every refusal that withholds Chat from a plan. */
-export const CHAT_PLAN_REQUIRED_MESSAGE = 'Chat is included on paid plans.'
+/**
+ * The card for a payer whose plan does not include Chat. Sim writes it at run admission, before
+ * any worker run exists, so it never crosses the worker billing protocol: its reason widens only
+ * the tag Sim renders, not the worker-shared card. The composer shows the same card before a send.
+ */
+export const CHAT_PLAN_REQUIRED_UPGRADE = {
+ reason: CHAT_PLAN_REQUIRED_REASON,
+ action: 'upgrade_plan',
+ message: 'Chat is included on paid plans. Upgrade your plan to talk to Sim.',
+} as const
diff --git a/apps/sim/lib/billing/usage-upgrade.ts b/apps/sim/lib/billing/usage-upgrade.ts
index 10f129f5927..73548119081 100644
--- a/apps/sim/lib/billing/usage-upgrade.ts
+++ b/apps/sim/lib/billing/usage-upgrade.ts
@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import type { UsageUpgradePayload } from '@/lib/api/contracts/subscription'
-import { CHAT_PLAN_REQUIRED_MESSAGE, CHAT_PLAN_REQUIRED_REASON } from '@/lib/billing/chat-plan'
+import type { CHAT_PLAN_REQUIRED_UPGRADE } from '@/lib/billing/chat-plan'
import type { AttributedUsageLimitsResult } from '@/lib/billing/core/attribution'
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
import type { BillingEntity } from '@/lib/billing/core/usage-log'
@@ -69,17 +69,6 @@ export async function resolveUsageUpgradePayload(
return { reason: 'usage_limit', action: 'increase_limit', message }
}
-/**
- * The card for a payer whose plan does not include Chat. Sim writes it at run admission, before
- * any worker run exists, so it never crosses the worker billing protocol: its reason widens only
- * the tag Sim renders, not the worker-shared card.
- */
-export const CHAT_PLAN_REQUIRED_UPGRADE = {
- reason: CHAT_PLAN_REQUIRED_REASON,
- action: 'upgrade_plan',
- message: `${CHAT_PLAN_REQUIRED_MESSAGE} Upgrade your plan to talk to Sim.`,
-} as const
-
/** Every card body the usage tag carries: the worker-shared card or Sim's Chat plan card. */
export type UsageUpgradeTagPayload = UsageUpgradePayload | typeof CHAT_PLAN_REQUIRED_UPGRADE
diff --git a/apps/sim/lib/mothership/jobs/dispatch.ts b/apps/sim/lib/mothership/jobs/dispatch.ts
index 91c02e34a6f..2ae476a49ea 100644
--- a/apps/sim/lib/mothership/jobs/dispatch.ts
+++ b/apps/sim/lib/mothership/jobs/dispatch.ts
@@ -1,12 +1,12 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { generateShortId } from '@sim/utils/id'
+import { CHAT_PLAN_REQUIRED_UPGRADE } from '@/lib/billing/chat-plan'
import {
checkAttributedUsageLimits,
resolveBillingAttribution,
} from '@/lib/billing/core/attribution'
import { payerHasChatAccess } from '@/lib/billing/core/chat-access'
-import { CHAT_PLAN_REQUIRED_UPGRADE } from '@/lib/billing/usage-upgrade'
import { isHosted } from '@/lib/core/config/env-flags'
import { acquireLock, releaseLock } from '@/lib/core/config/redis'
import { asOrchestrationError } from '@/lib/core/orchestration/types'
diff --git a/apps/sim/lib/mothership/request/tools/billing.ts b/apps/sim/lib/mothership/request/tools/billing.ts
index 93fd55d4b9b..ec4f82e6b00 100644
--- a/apps/sim/lib/mothership/request/tools/billing.ts
+++ b/apps/sim/lib/mothership/request/tools/billing.ts
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
+import { CHAT_PLAN_REQUIRED_UPGRADE } from '@/lib/billing/chat-plan'
import {
- CHAT_PLAN_REQUIRED_UPGRADE,
formatUsageUpgradeTag,
resolveUsageUpgradePayload,
type UsageUpgradeTagPayload,