diff --git a/surfsense_web/components/assistant-ui/thread.tsx b/surfsense_web/components/assistant-ui/thread.tsx
index c8851acc6..639ef8a89 100644
--- a/surfsense_web/components/assistant-ui/thread.tsx
+++ b/surfsense_web/components/assistant-ui/thread.tsx
@@ -128,11 +128,9 @@ const ThreadContent: FC = () => {
style={{ paddingBottom: "max(1rem, env(safe-area-inset-bottom))" }}
>
- !thread.isEmpty}>
-
-
-
-
+ !thread.isEmpty}>
+
+
@@ -558,23 +556,15 @@ const Composer: FC = () => {
// Submit message (blocked during streaming, document picker open, or AI responding to another user)
const handleSubmit = useCallback(() => {
- if (isThreadRunning || isBlockedByOtherUser) {
- return;
- }
- if (!showDocumentPopover && !showPromptPicker) {
- if (clipboardInitialText) {
- const userText = editorRef.current?.getText() ?? "";
- const combined = userText ? `${userText}\n\n${clipboardInitialText}` : clipboardInitialText;
- aui.composer().setText(combined);
- setClipboardInitialText(undefined);
- }
- aui.composer().send();
- editorRef.current?.clear();
- setMentionedDocuments([]);
- setSidebarDocs([]);
- }
if (isThreadRunning || isBlockedByOtherUser) return;
- if (showDocumentPopover) return;
+ if (showDocumentPopover || showPromptPicker) return;
+
+ if (clipboardInitialText) {
+ const userText = editorRef.current?.getText() ?? "";
+ const combined = userText ? `${userText}\n\n${clipboardInitialText}` : clipboardInitialText;
+ aui.composer().setText(combined);
+ setClipboardInitialText(undefined);
+ }
const viewportEl = viewportRef.current;
const heightBefore = viewportEl?.scrollHeight ?? 0;
@@ -588,18 +578,14 @@ const Composer: FC = () => {
// assistant message so that scrolling-to-bottom actually positions the
// user message at the TOP of the viewport. That slack height is
// calculated asynchronously (ResizeObserver → style → layout).
- //
- // We poll via rAF for ~2 s, re-scrolling whenever scrollHeight changes
- // (user msg render → assistant placeholder → ViewportSlack min-height →
- // first streamed content). Backup setTimeout calls cover cases where
- // the batcher's 50 ms throttle delays the DOM update past the rAF.
+ // Poll via rAF for ~500ms, re-scrolling whenever scrollHeight changes.
const scrollToBottom = () =>
threadViewportStore.getState().scrollToBottom({ behavior: "instant" });
let lastHeight = heightBefore;
let frames = 0;
let cancelled = false;
- const POLL_FRAMES = 120;
+ const POLL_FRAMES = 30;
const pollAndScroll = () => {
if (cancelled) return;
@@ -619,16 +605,11 @@ const Composer: FC = () => {
const t1 = setTimeout(scrollToBottom, 100);
const t2 = setTimeout(scrollToBottom, 300);
- const t3 = setTimeout(scrollToBottom, 600);
- // Cleanup if component unmounts during the polling window. The ref is
- // checked inside pollAndScroll; timeouts are cleared in the return below.
- // Store cleanup fn so it can be called from a useEffect cleanup if needed.
submitCleanupRef.current = () => {
cancelled = true;
clearTimeout(t1);
clearTimeout(t2);
- clearTimeout(t3);
};
}, [
showDocumentPopover,