fix: chat messages saving broken (#730)

1. New chat messages saving to old thread (race condition)
2. Feedback modal null handling issue (boolean | null not coerced to boolean)
3. Wrong icon on Integrations tab (was Cable, now Sun)
4. Wrong icon on Graph tab (was LayoutGridIcon, now GraphIcon)
  5. Missing cursor pointer on header tabs
  6. Default view was "graph" instead of "list"
This commit is contained in:
Prasanna721 2026-02-11 00:25:35 +00:00
parent 727f177953
commit b439e7ea01
7 changed files with 136 additions and 114 deletions

View file

@ -14,6 +14,7 @@ import Support from "@/components/new/settings/support"
import { useRouter } from "next/navigation"
import { useIsMobile } from "@hooks/use-mobile"
import { analytics } from "@/lib/analytics"
import { Sun } from "lucide-react"
const TABS = ["account", "integrations", "connections", "support"] as const
type SettingsTab = (typeof TABS)[number]
@ -52,23 +53,7 @@ const NAV_ITEMS: NavItem[] = [
id: "integrations",
label: "Integrations",
description: "Save, sync and search memories across tools",
icon: (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="12" cy="12" r="3" />
<path d="M12 1v4M12 19v4M4.22 4.22l2.83 2.83M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" />
</svg>
),
icon: <Sun className="size-5" />,
},
{
id: "connections",

View file

@ -138,16 +138,36 @@ export function ChatSidebar({
)
const pendingFollowUpGenerations = useRef<Set<string>>(new Set())
const messagesContainerRef = useRef<HTMLDivElement>(null)
const sentQueuedMessageRef = useRef<string | null>(null)
const { selectedProject } = useProject()
const { viewMode } = useViewMode()
const { user } = useAuth()
const [threadId, setThreadId] = useQueryState("thread", threadParam)
const fallbackChatId = useMemo(() => generateId(), [])
const [fallbackChatId, setFallbackChatId] = useState(() => generateId())
const currentChatId = threadId ?? fallbackChatId
const chatIdRef = useRef(currentChatId)
chatIdRef.current = currentChatId
const setCurrentChatId = useCallback(
(id: string) => setThreadId(id),
[setThreadId],
)
const chatTransport = useMemo(
() =>
new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
prepareSendMessagesRequest: ({ messages }) => ({
body: {
messages,
metadata: {
chatId: chatIdRef.current,
projectId: selectedProject,
},
},
}),
}),
[currentChatId, selectedProject],
)
const [pendingThreadLoad, setPendingThreadLoad] = useState<{
id: string
messages: UIMessage[]
@ -174,10 +194,7 @@ export function ChatSidebar({
const { messages, sendMessage, status, setMessages, stop } = useChat({
id: currentChatId ?? undefined,
transport: new DefaultChatTransport({
api: `${process.env.NEXT_PUBLIC_BACKEND_URL}/chat/v2`,
credentials: "include",
}),
transport: chatTransport,
onFinish: async (result) => {
if (result.message.role !== "assistant") return
@ -376,9 +393,13 @@ export function ChatSidebar({
const handleNewChat = useCallback(() => {
analytics.newChatCreated()
const newChatId = generateId()
chatIdRef.current = newChatId
setMessages([])
setThreadId(null)
setFallbackChatId(newChatId)
setInput("")
}, [setThreadId])
}, [setThreadId, setMessages])
const fetchThreads = useCallback(async () => {
setIsLoadingThreads(true)
@ -492,14 +513,23 @@ export function ChatSidebar({
isChatOpen &&
queuedMessage &&
status !== "submitted" &&
status !== "streaming"
status !== "streaming" &&
sentQueuedMessageRef.current !== queuedMessage
) {
sentQueuedMessageRef.current = queuedMessage
analytics.chatMessageSent({ source: "highlight" })
sendMessage({ text: queuedMessage })
onConsumeQueuedMessage?.()
}
}, [isChatOpen, queuedMessage, status, sendMessage, onConsumeQueuedMessage])
// Reset the sent message ref when queued message is consumed
useEffect(() => {
if (!queuedMessage) {
sentQueuedMessageRef.current = null
}
}, [queuedMessage])
// Scroll to bottom when a new user message is added
useEffect(() => {
const lastMessage = messages[messages.length - 1]
@ -867,12 +897,11 @@ export function ChatSidebar({
)}
</div>
))}
{(status === "submitted" || status === "streaming") &&
messages[messages.length - 1]?.role === "user" && (
<div className="flex gap-2">
<SuperLoader label="Thinking..." />
</div>
)}
{(status === "submitted" || status === "streaming") && (
<div className="flex gap-2">
<SuperLoader label="Thinking..." />
</div>
)}
</div>
</div>

View file

@ -11,7 +11,7 @@ import {
Settings,
Home,
Code2,
Cable,
Sun,
ExternalLink,
HelpCircle,
MenuIcon,
@ -22,6 +22,7 @@ import { Button } from "@ui/components/button"
import { cn } from "@lib/utils"
import { dmSansClassName } from "@/lib/fonts"
import { Tabs, TabsList, TabsTrigger } from "@ui/components/tabs"
import { GraphIcon } from "@/components/new/integration-icons"
import {
DropdownMenu,
DropdownMenuContent,
@ -48,18 +49,18 @@ interface HeaderProps {
onOpenSearch?: () => void
}
export function Header({
onAddMemory,
onOpenChat,
onOpenSearch,
}: HeaderProps) {
export function Header({ onAddMemory, onOpenChat, onOpenSearch }: HeaderProps) {
const { user } = useAuth()
const { selectedProject } = useProject()
const { switchProject } = useProjectMutations()
const router = useRouter()
const isMobile = useIsMobile()
const { resetOrgOnboarded } = useOrgOnboarding()
const [isFeedbackOpen, setIsFeedbackOpen] = useQueryState("feedback", feedbackParam)
const [feedbackOpen, setFeedbackOpen] = useQueryState(
"feedback",
feedbackParam,
)
const isFeedbackOpen = feedbackOpen ?? false
const { viewMode, setViewMode } = useViewMode()
const handleTryOnboarding = () => {
@ -67,9 +68,7 @@ export function Header({
router.push("/onboarding?step=input&flow=welcome")
}
const handleFeedback = () => {
setIsFeedbackOpen(true)
}
const handleFeedback = () => setFeedbackOpen(true)
const displayName =
user?.displayUsername ||
@ -155,13 +154,15 @@ export function Header({
{!isMobile && (
<Tabs
value={viewMode === "list" ? "grid" : viewMode}
onValueChange={(v) => setViewMode(v === "grid" ? "list" : v as "graph" | "integrations")}
onValueChange={(v) =>
setViewMode(v === "grid" ? "list" : (v as "graph" | "integrations"))
}
>
<TabsList className="rounded-full border border-[#161F2C] h-11! z-10!">
<TabsTrigger
value="grid"
className={cn(
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
@ -171,21 +172,21 @@ export function Header({
<TabsTrigger
value="graph"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<LayoutGridIcon className="size-4" />
<GraphIcon className="size-4" />
Graph
</TabsTrigger>
<TabsTrigger
value="integrations"
className={cn(
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4",
"rounded-full dark:data-[state=active]:bg-[#00173C]! dark:data-[state=active]:border-[#2261CA33]! px-4 py-4 cursor-pointer",
dmSansClassName(),
)}
>
<Cable className="size-4" />
<Sun className="size-4" />
Integrations
</TabsTrigger>
</TabsList>
@ -232,7 +233,7 @@ export function Header({
onClick={() => setViewMode("integrations")}
className="px-3 py-2.5 rounded-md hover:bg-[#293952]/40 cursor-pointer text-white text-sm font-medium gap-2"
>
<Cable className="h-4 w-4 text-[#737373]" />
<Sun className="h-4 w-4 text-[#737373]" />
Integrations
</DropdownMenuItem>
<DropdownMenuItem
@ -410,7 +411,7 @@ export function Header({
</div>
<FeedbackModal
isOpen={isFeedbackOpen}
onClose={() => setIsFeedbackOpen(false)}
onClose={() => setFeedbackOpen(false)}
/>
</div>
)

View file

@ -1,4 +1,7 @@
import Image from "next/image"
import { Rotate3d } from "lucide-react"
export { Rotate3d as GraphIcon }
export function ChromeIcon({ className }: { className?: string }) {
return (

View file

@ -2,7 +2,7 @@
import { useState } from "react"
import { cn } from "@lib/utils"
import { dmSansClassName, dmSans125ClassName } from "@/lib/fonts"
import { dmSansClassName } from "@/lib/fonts"
import { Button } from "@ui/components/button"
import { MCPDetailView } from "@/components/new/mcp-modal/mcp-detail-view"
import { XBookmarksDetailView } from "@/components/new/onboarding/x-bookmarks-detail-view"
@ -17,7 +17,7 @@ import {
RaycastIcon,
} from "@/components/new/integration-icons"
import { GoogleDrive, Notion, OneDrive } from "@ui/assets/icons"
import { Cable, ArrowLeft } from "lucide-react"
import { ArrowLeft, Sun } from "lucide-react"
import Image from "next/image"
type CardId =
@ -45,9 +45,27 @@ const cards: IntegrationCardDef[] = [
pro: true,
icon: (
<div className="flex items-center -space-x-1.5">
<Image src="/images/plugins/claude-code.svg" alt="Claude Code" width={24} height={24} className="size-6 rounded" />
<Image src="/images/plugins/opencode.svg" alt="OpenCode" width={24} height={24} className="size-6 rounded" />
<Image src="/images/plugins/clawdbot.svg" alt="ClawdBot" width={24} height={24} className="size-6 rounded" />
<Image
src="/images/plugins/claude-code.svg"
alt="Claude Code"
width={24}
height={24}
className="size-6 rounded"
/>
<Image
src="/images/plugins/opencode.svg"
alt="OpenCode"
width={24}
height={24}
className="size-6 rounded"
/>
<Image
src="/images/plugins/clawdbot.svg"
alt="ClawdBot"
width={24}
height={24}
className="size-6 rounded"
/>
</div>
),
},
@ -69,11 +87,7 @@ const cards: IntegrationCardDef[] = [
title: "Connect to AI",
description: "Set up MCP to use your memory in Cursor, Claude, and more",
icon: (
<img
src="/onboarding/mcp.png"
alt="MCP"
className="size-20 h-auto"
/>
<img src="/onboarding/mcp.png" alt="MCP" className="size-20 h-auto" />
),
},
{
@ -98,9 +112,7 @@ const cards: IntegrationCardDef[] = [
id: "import",
title: "Import Bookmarks",
description: "Bring in X/Twitter bookmarks and turn them into memories",
icon: (
<img src="/onboarding/x.png" alt="X" className="size-10" />
),
icon: <img src="/onboarding/x.png" alt="X" className="size-10" />,
},
]
@ -131,46 +143,43 @@ function DetailWrapper({
export function IntegrationsView() {
const [selectedCard, setSelectedCard] = useState<CardId | null>(null)
if (selectedCard === "mcp") {
return <MCPDetailView onBack={() => setSelectedCard(null)} />
}
if (selectedCard === "import") {
return <XBookmarksDetailView onBack={() => setSelectedCard(null)} />
}
if (selectedCard === "chrome") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ChromeDetail />
</DetailWrapper>
)
}
if (selectedCard === "shortcuts") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ShortcutsDetail />
</DetailWrapper>
)
}
if (selectedCard === "raycast") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<RaycastDetail />
</DetailWrapper>
)
}
if (selectedCard === "connections") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<ConnectionsDetail />
</DetailWrapper>
)
}
if (selectedCard === "plugins") {
return (
<DetailWrapper onBack={() => setSelectedCard(null)}>
<PluginsDetail />
</DetailWrapper>
)
const handleBack = () => setSelectedCard(null)
switch (selectedCard) {
case "mcp":
return <MCPDetailView onBack={handleBack} />
case "import":
return <XBookmarksDetailView onBack={handleBack} />
case "chrome":
return (
<DetailWrapper onBack={handleBack}>
<ChromeDetail />
</DetailWrapper>
)
case "shortcuts":
return (
<DetailWrapper onBack={handleBack}>
<ShortcutsDetail />
</DetailWrapper>
)
case "raycast":
return (
<DetailWrapper onBack={handleBack}>
<RaycastDetail />
</DetailWrapper>
)
case "connections":
return (
<DetailWrapper onBack={handleBack}>
<ConnectionsDetail />
</DetailWrapper>
)
case "plugins":
return (
<DetailWrapper onBack={handleBack}>
<PluginsDetail />
</DetailWrapper>
)
}
return (
@ -178,15 +187,10 @@ export function IntegrationsView() {
<div className="max-w-3xl mx-auto">
<div className="mb-6 space-y-1">
<div className="flex items-center gap-2">
<Cable className="size-5 text-[#4BA0FA]" />
<Sun className="size-5 text-white" />
<h2 className="text-white text-xl font-medium">Integrations</h2>
</div>
<p
className={cn(
"text-[#8B8B8B] text-sm",
dmSansClassName(),
)}
>
<p className={cn("text-[#8B8B8B] text-sm", dmSansClassName())}>
Connect supermemory to your tools and workflows
</p>
</div>
@ -215,9 +219,7 @@ export function IntegrationsView() {
{card.icon}
</div>
<div className="flex-1">
<h3 className="text-white text-sm font-medium">
{card.title}
</h3>
<h3 className="text-white text-sm font-medium">{card.title}</h3>
<p
className={cn(
"text-[#8B8B8B] text-xs leading-relaxed mt-0.5",

View file

@ -25,6 +25,8 @@ export const feedbackParam = parseAsBoolean.withDefault(false)
// View & filter states
const viewLiterals = ["graph", "list", "integrations"] as const
export type ViewParamValue = (typeof viewLiterals)[number]
export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("graph")
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault([])
export const viewParam = parseAsStringLiteral(viewLiterals).withDefault("list")
export const categoriesParam = parseAsArrayOf(parseAsString, ",").withDefault(
[],
)
export const projectParam = parseAsString.withDefault("sm_project_default")

View file

@ -58,7 +58,7 @@ function DialogContent({
<DialogOverlay />
<DialogPrimitive.Content
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-100 sm:max-w-lg",
className,
)}
data-slot="dialog-content"