diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx
index 0cee0d078..9e03c2577 100644
--- a/surfsense_web/components/assistant-ui/thread.tsx
+++ b/surfsense_web/components/assistant-ui/thread.tsx
@@ -24,7 +24,7 @@ import {
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import Image from "next/image";
-import { useParams } from "next/navigation";
+import { useParams, useRouter } from "next/navigation";
import { type FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
agentToolsAtom,
@@ -444,6 +444,52 @@ const ClipboardChip: FC<{ text: string; onDismiss: () => void }> = ({ text, onDi
);
};
+/**
+ * Inline "this workspace can't chat yet" tray, mirrored on top of the composer
+ * (same visual treatment as the "Connect your tools" tray below it).
+ *
+ * Replaces the old full-screen onboarding redirect: a workspace with no usable
+ * chat model (fresh install, or an admin deleted the last model) renders the
+ * composer in place with this notice instead of navigating away. Configurable
+ * members get a CTA to connect one; everyone else is told to ask an admin. The
+ * send button stays disabled independently, and the backend rejects any send
+ * that slips through, so this is purely guidance.
+ *
+ * Presentational: visibility is decided by the parent so the composer can flatten
+ * its top shadow for a seamless join.
+ */
+const ChatUnavailableNotice: FC<{ workspaceId: number; canConfigure: boolean }> = ({
+ workspaceId,
+ canConfigure,
+}) => {
+ const router = useRouter();
+
+ return (
+
+
+
+
+ {canConfigure
+ ? "Connect a language model to start chatting."
+ : "No model available. Ask a workspace admin to connect a language model."}
+
+
@@ -1151,10 +1207,10 @@ const ComposerAction: FC = ({
hydrateDisabled();
}, [hydrateDisabled]);
- // Defense-in-depth only: the onboarding gate makes an unconfigured
- // workspace unreachable, but a stale cache (e.g. another admin removed the
- // last model) can briefly leave this composer mounted. Block sends silently
- // until the status refetch re-engages the gate.
+ // A workspace with no usable chat model renders the composer with an inline
+ // notice (see ChatUnavailableNotice) rather than being redirected away, so
+ // the send button must stay disabled here. The backend also rejects any
+ // send that lacks a resolvable model, making this defense-in-depth.
const isWorkspaceChatReady = setupStatus?.status === "ready";
const isSendDisabled = isComposerEmpty || !isWorkspaceChatReady || isBlockedByOtherUser;