From 2ba9c8a996bcdf8d13307a7fd701e01e2889cdef Mon Sep 17 00:00:00 2001 From: Usama Date: Thu, 30 Jul 2026 16:54:36 +0000 Subject: [PATCH] - Extract ChatComposerShell, ConversationFocusChips, ConversationPickerButton, and ChatTemplatesMenuConnected so classic, agentic, and Ask home chat share one composer, chips, and templates-menu implementation instead of three drifting copies - Fix classic Specific Details to show "Context added" instantly on send, replacing a fragile history-refetch race with a local stand-in that's swapped for the real message once history reconciles - Fix scroll-to-bottom detection for chats that render their thread after an initial loading/mode-picker screen; the anchor-wait path now uses a scoped MutationObserver (via a new data-app-scroll-root marker) instead of a busy per-frame poll - Give agentic template pills their mode-accent border and fix long labels overflowing instead of truncating - Add a count-only mode to ConversationFocusChips and use it on the New Chat picker screen, removing a duplicated hand-rolled chips block - Add a disabled prop to ConversationFocusChips, separate from isClearing, so blocking the control for unrelated reasons doesn't misuse the clear-action's own loading state - Revert the agentic error-message masking added earlier in this branch (agentic_worker.py) to redo it together with a related prompt fix, rather than ship a half-finished whitelist - Add test coverage for the previously untested ConversationFocusChips precedence rules and the useStickToBottom hook's container resolution, scroll-target selection, and resize/mutation-driven remeasurement --- .../src/components/chat/AgenticChatPanel.tsx | 325 +++++------- .../src/components/chat/AgenticIntroModal.tsx | 90 ++++ .../src/components/chat/ChatComposer.test.tsx | 181 +++++++ .../src/components/chat/ChatComposer.tsx | 126 +++++ .../src/components/chat/ChatTemplatesMenu.tsx | 23 +- .../chat/ChatTemplatesMenuConnected.tsx | 213 ++++++++ .../src/components/chat/hooks/index.ts | 52 +- .../src/components/common/ScrollToBottom.tsx | 20 +- .../components/common/scrollPosition.test.ts | 85 ++++ .../src/components/common/scrollPosition.ts | 51 ++ .../common/useStickToBottom.test.ts | 261 ++++++++++ .../src/components/common/useStickToBottom.ts | 126 +++++ .../ConversationTranscriptSection.tsx | 8 +- .../src/components/layout/BaseLayout.tsx | 8 +- .../ParticipantConversationAudio.tsx | 6 +- .../src/routes/project/chat/NewChatRoute.tsx | 329 ++++++------ .../routes/project/chat/ProjectChatRoute.tsx | 471 +++++++----------- echo/server/dembrane/api/v2/bff/chats.py | 6 +- echo/server/tests/test_bff_chats_search.py | 90 ++++ 19 files changed, 1809 insertions(+), 662 deletions(-) create mode 100644 echo/frontend/src/components/chat/AgenticIntroModal.tsx create mode 100644 echo/frontend/src/components/chat/ChatComposer.test.tsx create mode 100644 echo/frontend/src/components/chat/ChatComposer.tsx create mode 100644 echo/frontend/src/components/chat/ChatTemplatesMenuConnected.tsx create mode 100644 echo/frontend/src/components/common/scrollPosition.test.ts create mode 100644 echo/frontend/src/components/common/scrollPosition.ts create mode 100644 echo/frontend/src/components/common/useStickToBottom.test.ts create mode 100644 echo/frontend/src/components/common/useStickToBottom.ts create mode 100644 echo/server/tests/test_bff_chats_search.py diff --git a/echo/frontend/src/components/chat/AgenticChatPanel.tsx b/echo/frontend/src/components/chat/AgenticChatPanel.tsx index 554d7b58c..b79a5c443 100644 --- a/echo/frontend/src/components/chat/AgenticChatPanel.tsx +++ b/echo/frontend/src/components/chat/AgenticChatPanel.tsx @@ -20,7 +20,6 @@ import { UnstyledButton, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { ChatCircleTextIcon } from "@phosphor-icons/react"; import { IconAlertCircle, IconChevronDown, @@ -39,13 +38,18 @@ import { useState, } from "react"; import { useLocation } from "react-router"; +import { + ChatComposerShell, + ConversationFocusChips, + ConversationPickerButton, +} from "@/components/chat/ChatComposer"; import { useChatHistory, useProjectChatContext, useUpdateChatMutation, } from "@/components/chat/hooks"; -import { InsertTemplateMenu } from "@/components/chat/InsertTemplateMenu"; import { consumeChatPrefill } from "@/components/chat/prefill"; +import { useStickToBottom } from "@/components/common/useStickToBottom"; import { ConversationLinks } from "@/components/conversation/ConversationLinks"; import { useClearChatContextMutation, @@ -54,8 +58,6 @@ import { import { ProjectConversationsPanel } from "@/components/conversation/ProjectConversationsPanel"; import { ErrorBoundary } from "@/components/error/ErrorBoundary"; import { GoalSuggestionCard } from "@/components/goal/GoalSuggestionCard"; -import { useElementOnScreen } from "@/hooks/useElementOnScreen"; -import { useI18nNavigate } from "@/hooks/useI18nNavigate"; import { useLanguage } from "@/hooks/useLanguage"; import { useWorkspace } from "@/hooks/useWorkspace"; import { useWorkspaceUsage } from "@/hooks/useWorkspaceUsage"; @@ -96,6 +98,7 @@ import { import { CanvasSuggestionCard } from "./CanvasSuggestionCard"; import { ChatAccordionItemMenu } from "./ChatAccordion"; import { ChatHistoryMessage } from "./ChatHistoryMessage"; +import { ChatTemplatesMenuConnected } from "./ChatTemplatesMenuConnected"; import { CustomVerificationTopicSuggestionCard } from "./CustomVerificationTopicSuggestionCard"; import { formatMessage } from "./chatUtils"; import { ChatTurnLimitCard, ChatUpgradeModal } from "./FreeTierChatGate"; @@ -564,14 +567,14 @@ const ToolActivityGroup = ({ } onClick={isSingle ? undefined : onToggle} > - - + + @@ -616,7 +619,6 @@ export const AgenticChatPanel = ({ const { iso639_1, language } = useLanguage(); const { workspace, workspaceId } = useWorkspace(); const location = useLocation(); - const navigate = useI18nNavigate(); // Seed question from the Ask home page (router state), consumed exactly once. const initialMessageRef = useRef( typeof (location.state as { initialMessage?: unknown } | null) @@ -665,11 +667,9 @@ export const AgenticChatPanel = ({ const stopArmedRunIdRef = useRef(null); const requestedStreamKeyRef = useRef(null); const currentRunIdRef = useRef(null); - const [scrollTargetRef, isVisible] = useElementOnScreen({ - root: null, - rootMargin: "-83px", - threshold: 0.1, - }); + const threadScrollRef = useRef(null); + const { isAtBottomRef, scrollToBottom, showScrollButton } = + useStickToBottom(threadScrollRef); useEffect(() => { currentRunIdRef.current = runId; @@ -1180,18 +1180,6 @@ export const AgenticChatPanel = ({ } }, [runStatus, stopStream]); - const scrollToBottom = useCallback( - (behavior: ScrollBehavior = "smooth") => { - window.requestAnimationFrame(() => { - scrollTargetRef.current?.scrollIntoView({ - behavior, - block: "end", - }); - }); - }, - [scrollTargetRef], - ); - // Stick to the bottom only when the reader is already there (or just sent a // message). Someone scrolled up to read must never be yanked back down by a // streaming event; the scroll-to-bottom button is their way back. Bottomness @@ -1200,10 +1188,6 @@ export const AgenticChatPanel = ({ // bounced against the stream. const hasScrolledInitiallyRef = useRef(false); const forceNextScrollRef = useRef(false); - const isAtBottomRef = useRef(true); - useEffect(() => { - isAtBottomRef.current = isVisible; - }, [isVisible]); // biome-ignore lint/correctness/useExhaustiveDependencies: chatId is the trigger, not a read — switching chats re-arms the initial jump useEffect(() => { hasScrolledInitiallyRef.current = false; @@ -1466,29 +1450,11 @@ export const AgenticChatPanel = ({ - - This is the new chat experience} - openDelay={300} - > - - - - - + + {error && ( )} -