diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
index bfe8599f6..284c1ebc0 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
@@ -265,7 +265,7 @@ export function DashboardClientLayout({
-
{children}
+ {children}
diff --git a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
index 00514facb..e18629b92 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/new-chat/[[...chat_id]]/page.tsx
@@ -626,11 +626,11 @@ export default function NewChatPage() {
-
-
-
-
-
+
+ }
+ />
);
diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx
index 8a7eca58e..6e69296e5 100644
--- a/surfsense_web/components/assistant-ui/thread.tsx
+++ b/surfsense_web/components/assistant-ui/thread.tsx
@@ -8,7 +8,6 @@ import {
ThreadPrimitive,
useAssistantState,
useMessage,
- useThreadViewport,
} from "@assistant-ui/react";
import { useAtomValue } from "jotai";
import {
@@ -69,6 +68,8 @@ import { cn } from "@/lib/utils";
*/
interface ThreadProps {
messageThinkingSteps?: Map
;
+ /** Optional header component to render at the top of the viewport (sticky) */
+ header?: React.ReactNode;
}
// Context to pass thinking steps to AssistantMessage
@@ -212,122 +213,25 @@ const ThinkingStepsDisplay: FC<{ steps: ThinkingStep[]; isThreadRunning?: boolea
);
};
-/**
- * Component that handles auto-scroll when thinking steps update.
- * Uses useThreadViewport to scroll to bottom when thinking steps change,
- * ensuring the user always sees the latest content during streaming.
- */
-const ThinkingStepsScrollHandler: FC = () => {
- const thinkingStepsMap = useContext(ThinkingStepsContext);
- const viewport = useThreadViewport();
- const isRunning = useAssistantState(({ thread }) => thread.isRunning);
- // Track the serialized state to detect any changes
- const prevStateRef = useRef("");
-
- useEffect(() => {
- // Only act during streaming
- if (!isRunning) {
- prevStateRef.current = "";
- return;
- }
-
- // Serialize the thinking steps state to detect any changes
- // This catches new steps, status changes, and item additions
- let stateString = "";
- thinkingStepsMap.forEach((steps, msgId) => {
- steps.forEach((step) => {
- stateString += `${msgId}:${step.id}:${step.status}:${step.items?.length || 0};`;
- });
- });
-
- // If state changed at all during streaming, scroll
- if (stateString !== prevStateRef.current && stateString !== "") {
- prevStateRef.current = stateString;
-
- // Multiple attempts to ensure scroll happens after DOM updates
- const scrollAttempt = () => {
- try {
- viewport.scrollToBottom();
- } catch (e) {
- // Ignore errors - viewport might not be ready
- }
- };
-
- // Delayed attempts to handle async DOM updates
- requestAnimationFrame(scrollAttempt);
- setTimeout(scrollAttempt, 100);
- }
- }, [thinkingStepsMap, viewport, isRunning]);
-
- return null; // This component doesn't render anything
-};
-
-/**
- * Component that handles auto-scroll when a new user query is submitted.
- * Scrolls to position the new user message at the top of the viewport,
- * similar to Cursor's behavior where the current query stays at the top.
- */
-const NewQueryScrollHandler: FC = () => {
- const messages = useAssistantState(({ thread }) => thread.messages);
- const prevMessageCountRef = useRef(0);
- const prevLastUserMsgIdRef = useRef("");
-
- useEffect(() => {
- const currentCount = messages.length;
-
- // Find the last user message
- const lastUserMessage = [...messages].reverse().find((m) => m.role === "user");
- const lastUserMsgId = lastUserMessage?.id || "";
-
- // Check if a new user message was added (not on initial load)
- if (
- prevMessageCountRef.current > 0 && // Not initial load
- currentCount > prevMessageCountRef.current &&
- lastUserMsgId !== prevLastUserMsgIdRef.current &&
- lastUserMsgId
- ) {
- // New user message added - scroll to make it visible at the top
- // Use multiple attempts to ensure the DOM has updated
- const scrollToNewQuery = () => {
- // Find the last user message element
- const userMessages = document.querySelectorAll('[data-role="user"]');
- const lastUserMsgElement = userMessages[userMessages.length - 1];
- if (lastUserMsgElement) {
- // Scroll so the user message is at the top of the viewport
- lastUserMsgElement.scrollIntoView({ behavior: "smooth", block: "start" });
- }
- };
-
- // Delayed attempts to handle async DOM updates
- requestAnimationFrame(scrollToNewQuery);
- setTimeout(scrollToNewQuery, 50);
- setTimeout(scrollToNewQuery, 150);
- }
-
- prevMessageCountRef.current = currentCount;
- prevLastUserMsgIdRef.current = lastUserMsgId;
- }, [messages]);
-
- return null; // This component doesn't render anything
-};
-
-export const Thread: FC = ({ messageThinkingSteps = new Map() }) => {
+export const Thread: FC = ({ messageThinkingSteps = new Map(), header }) => {
return (
- {/* Auto-scroll handler for new queries - positions new user messages at the top */}
-
- {/* Auto-scroll handler for thinking steps - must be inside Viewport */}
-
+ {/* Optional sticky header for model selector etc. */}
+ {header && (
+
+ {header}
+
+ )}
thread.isEmpty}>
diff --git a/surfsense_web/components/new-chat/chat-header.tsx b/surfsense_web/components/new-chat/chat-header.tsx
index 1e0259447..34b2cc814 100644
--- a/surfsense_web/components/new-chat/chat-header.tsx
+++ b/surfsense_web/components/new-chat/chat-header.tsx
@@ -47,12 +47,7 @@ export function ChatHeader({ searchSpaceId }: ChatHeaderProps) {
return (
<>
- {/* Header Bar */}
-
-
-
-
- {/* Config Sidebar */}
+