From 5f8b9ed98e53d5a933f9b4e0508968fa15cebc4e Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 12 Mar 2026 21:49:25 -0700 Subject: [PATCH] fix: remove hero animations and ensure new chat tab always exists - Remove framer-motion animations from hero greeting, input bar, and suggestion buttons so they render instantly - Fix SSR hydration mismatch by deferring hero render until client mount - Always create a "New Chat" tab on fresh load and when closing last tab Made-with: Cursor --- apps/web/app/components/chat-message.tsx | 6 +- apps/web/app/components/chat-panel.tsx | 88 +++++-------------- .../tiptap/file-mention-extension.ts | 2 +- apps/web/app/workspace/workspace-content.tsx | 46 +++++++++- 4 files changed, 67 insertions(+), 75 deletions(-) diff --git a/apps/web/app/components/chat-message.tsx b/apps/web/app/components/chat-message.tsx index 0319ff5be95..d02123c6be2 100644 --- a/apps/web/app/components/chat-message.tsx +++ b/apps/web/app/components/chat-message.tsx @@ -777,12 +777,14 @@ export const ChatMessage = memo(function ChatMessage({ message, isStreaming, onS const attachmentInfo = parseAttachments(textContent); const richHtml = userHtmlMap?.get(message.id) ?? userHtmlMap?.get(textContent) ?? userHtmlMap?.get(attachmentInfo?.message ?? ""); - const bubbleContent =

{attachmentInfo?.message ?? textContent}

; + const bubbleContent = richHtml + ?
+ :

{attachmentInfo?.message ?? textContent}

; if (attachmentInfo) { return (
- + {!richHtml && } {(attachmentInfo.message || richHtml) && (
( const [showFilePicker, setShowFilePicker] = useState(false); + const [mounted, setMounted] = useState(false); + useEffect(() => { setMounted(true); }, []); + // ── Reconnection state ── const [isReconnecting, setIsReconnecting] = useState(false); const reconnectAbortRef = useRef(null); @@ -884,10 +886,13 @@ export const ChatPanel = forwardRef( const [rawView, _setRawView] = useState(false); // ── Hero state (new chat screen) ── - const [greeting, setGreeting] = useState(""); - const [visiblePrompts, setVisiblePrompts] = useState([]); + const [greeting, setGreeting] = useState("How can I help?"); + const [visiblePrompts, setVisiblePrompts] = useState(PROMPT_SUGGESTIONS.slice(0, 7)); + const heroInitRef = useRef(false); useEffect(() => { + if (heroInitRef.current) return; + heroInitRef.current = true; const greetings = [ "Ready to build?", "Let's automate something?", @@ -904,9 +909,6 @@ export const ChatPanel = forwardRef( }; const allGreetings = [getTimeGreeting(), ...greetings]; setGreeting(allGreetings[Math.floor(Math.random() * allGreetings.length)]); - }, []); - - useEffect(() => { const shuffled = [...PROMPT_SUGGESTIONS].sort(() => 0.5 - Math.random()); setVisiblePrompts(shuffled.slice(0, 7)); }, []); @@ -2156,7 +2158,6 @@ export const ChatPanel = forwardRef( // ── Render ── return ( -
(
{/* Messages */}
(

+ ) : (showHeroState && !mounted) ? ( +
) : showHeroState ? (
{/* Hero greeting */} {greeting && ( - - {greeting.split(" ").map((word, i) => ( - - {word} - - ))} - + {greeting} + )} {/* Centered input bar */} - - - {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} - - +
+ {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} +
{/* Prompt suggestion pills */} - +
{visiblePrompts.slice(0, 3).map((template) => { const Icon = template.icon; @@ -2448,7 +2407,7 @@ export const ChatPanel = forwardRef( ); })}
- +
) : messages.length === 0 ? (
@@ -2546,13 +2505,7 @@ export const ChatPanel = forwardRef( style={{ background: "var(--color-bg-glass)" }} >
- - {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)} - + {inputBarContainer(handleInputDragOver, handleInputDragLeave, handleInputDrop)}
)} @@ -2569,7 +2522,6 @@ export const ChatPanel = forwardRef( )}
- ); }, ); diff --git a/apps/web/app/components/tiptap/file-mention-extension.ts b/apps/web/app/components/tiptap/file-mention-extension.ts index 591e14fc931..99cae350025 100644 --- a/apps/web/app/components/tiptap/file-mention-extension.ts +++ b/apps/web/app/components/tiptap/file-mention-extension.ts @@ -83,7 +83,7 @@ export const FileMentionNode = Node.create({ }, HTMLAttributes, ), - `@${label}`, + label, ]; }, }); diff --git a/apps/web/app/workspace/workspace-content.tsx b/apps/web/app/workspace/workspace-content.tsx index c0559a5b325..66758ace0b4 100644 --- a/apps/web/app/workspace/workspace-content.tsx +++ b/apps/web/app/workspace/workspace-content.tsx @@ -529,7 +529,17 @@ function WorkspacePageInner() { if (tabLoadedForWorkspace.current === key) return; tabLoadedForWorkspace.current = key; const loaded = loadTabs(key); - setTabState(loaded); + const hasNonHomeTabs = loaded.tabs.some((t) => t.id !== HOME_TAB_ID); + if (!hasNonHomeTabs) { + const newTab: Tab = { + id: generateTabId(), + type: "chat", + title: "New Chat", + }; + setTabState(openTab(loaded, newTab)); + } else { + setTabState(loaded); + } }, [workspaceName]); // Persist tabs to localStorage on change (only after initial load for this workspace) @@ -1017,7 +1027,25 @@ function WorkspacePageInner() { const handleTabClose = useCallback((tabId: string) => { const prev = tabState; - const next = closeTab(prev, tabId); + let next = closeTab(prev, tabId); + const hasNonHomeTabs = next.tabs.some((t) => t.id !== HOME_TAB_ID); + if (!hasNonHomeTabs) { + const newTab: Tab = { + id: generateTabId(), + type: "chat", + title: "New Chat", + }; + next = openTab(next, newTab); + setTabState(next); + setActivePath(null); + setContent({ kind: "none" }); + setActiveSessionId(null); + setActiveSubagentKey(null); + requestAnimationFrame(() => { + void chatRef.current?.newSession(); + }); + return; + } setTabState(next); if (next.activeTabId !== prev.activeTabId) { const newActive = next.tabs.find((t) => t.id === next.activeTabId); @@ -1062,10 +1090,20 @@ function WorkspacePageInner() { const handleTabCloseAll = useCallback(() => { setTabState((prev) => { - const next = closeAllTabs(prev); + const closed = closeAllTabs(prev); setActivePath(null); setContent({ kind: "none" }); - return next; + setActiveSessionId(null); + setActiveSubagentKey(null); + const newTab: Tab = { + id: generateTabId(), + type: "chat", + title: "New Chat", + }; + return openTab(closed, newTab); + }); + requestAnimationFrame(() => { + void chatRef.current?.newSession(); }); }, []);