refactor: implement chat tab removal functionality and enhance tab title update logic for improved user experience

This commit is contained in:
Anish Sarkar 2026-03-29 21:34:41 +05:30
parent 7632291731
commit b54aa517a8
4 changed files with 59 additions and 4 deletions

View file

@ -128,6 +128,21 @@ export const updateChatTabTitleAtom = atom(
(get, set, { chatId, title }: { chatId: number; title: string }) => {
const state = get(tabsStateAtom);
const tabId = makeChatTabId(chatId);
const hasExactTab = state.tabs.some((t) => t.id === tabId);
// During lazy thread creation, title updates can arrive before "chat-new"
// is swapped to chat-{id}. In that case, promote the active "chat-new" tab.
if (!hasExactTab && state.activeTabId === "chat-new") {
set(tabsStateAtom, {
...state,
activeTabId: tabId,
tabs: state.tabs.map((t) =>
t.id === "chat-new" ? { ...t, id: tabId, chatId, title } : t
),
});
return;
}
set(tabsStateAtom, {
...state,
tabs: state.tabs.map((t) => (t.id === tabId ? { ...t, title } : t)),
@ -213,6 +228,34 @@ export const closeTabAtom = atom(null, (get, set, tabId: string) => {
return remaining.find((t) => t.id === newActiveId) ?? null;
});
/** Remove a chat tab by chat ID (used when a chat is deleted). */
export const removeChatTabAtom = atom(null, (get, set, chatId: number) => {
const state = get(tabsStateAtom);
const tabId = makeChatTabId(chatId);
const idx = state.tabs.findIndex((t) => t.id === tabId);
if (idx === -1) return null;
const remaining = state.tabs.filter((t) => t.id !== tabId);
// Always keep at least one tab available.
if (remaining.length === 0) {
set(tabsStateAtom, {
tabs: [INITIAL_CHAT_TAB],
activeTabId: "chat-new",
});
return INITIAL_CHAT_TAB;
}
let newActiveId = state.activeTabId;
if (state.activeTabId === tabId) {
const newIdx = Math.min(idx, remaining.length - 1);
newActiveId = remaining[newIdx].id;
}
set(tabsStateAtom, { tabs: remaining, activeTabId: newActiveId });
return remaining.find((t) => t.id === newActiveId) ?? null;
});
/** Reset tabs when switching search spaces. */
export const resetTabsAtom = atom(null, (_get, set) => {
set(tabsStateAtom, { ...initialState });

View file

@ -20,7 +20,7 @@ import {
teamDialogAtom,
userSettingsDialogAtom,
} from "@/atoms/settings/settings-dialog.atoms";
import { resetTabsAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { removeChatTabAtom, resetTabsAtom, syncChatTabAtom, type Tab } from "@/atoms/tabs/tabs.atom";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
import { MorePagesDialog } from "@/components/settings/more-pages-dialog";
import { SearchSpaceSettingsDialog } from "@/components/settings/search-space-settings-dialog";
@ -103,6 +103,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const resetCurrentThread = useSetAtom(resetCurrentThreadAtom);
const syncChatTab = useSetAtom(syncChatTabAtom);
const resetTabs = useSetAtom(resetTabsAtom);
const removeChatTab = useSetAtom(removeChatTabAtom);
// State for handling new chat navigation when router is out of sync
const [pendingNewChat, setPendingNewChat] = useState(false);
@ -325,7 +326,8 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
const thread = threadsData?.threads?.find((t) => t.id === chatId);
syncChatTab({
chatId,
title: thread?.title || (chatId ? `Chat ${chatId}` : "New Chat"),
// Avoid overwriting live SSE-updated tab titles with fallback values.
title: chatId ? (thread?.title ?? undefined) : "New Chat",
chatUrl,
});
}, [currentChatId, searchSpaceId, threadsData?.threads, syncChatTab]);
@ -637,6 +639,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
setIsDeletingChat(true);
try {
await deleteThread(chatToDelete.id);
removeChatTab(chatToDelete.id);
queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] });
if (currentChatId === chatToDelete.id) {
resetCurrentThread();
@ -664,6 +667,7 @@ export function LayoutDataProvider({ searchSpaceId, children }: LayoutDataProvid
currentThreadState.id,
params?.chat_id,
router,
removeChatTab,
]);
// Rename handler

View file

@ -1,6 +1,7 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import { format } from "date-fns";
import {
ArchiveIcon,
@ -41,6 +42,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press";
import { useIsMobile } from "@/hooks/use-mobile";
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
import {
deleteThread,
fetchThreads,
@ -70,6 +72,7 @@ export function AllPrivateChatsSidebarContent({
const params = useParams();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const removeChatTab = useSetAtom(removeChatTabAtom);
const currentChatId = Array.isArray(params.chat_id)
? Number(params.chat_id[0])
@ -158,6 +161,7 @@ export function AllPrivateChatsSidebarContent({
setDeletingThreadId(threadId);
try {
await deleteThread(threadId);
removeChatTab(threadId);
toast.success(t("chat_deleted") || "Chat deleted successfully");
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
@ -176,7 +180,7 @@ export function AllPrivateChatsSidebarContent({
setDeletingThreadId(null);
}
},
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange]
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange, removeChatTab]
);
const handleToggleArchive = useCallback(

View file

@ -1,6 +1,7 @@
"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useSetAtom } from "jotai";
import { format } from "date-fns";
import {
ArchiveIcon,
@ -41,6 +42,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip
import { useDebouncedValue } from "@/hooks/use-debounced-value";
import { useLongPress } from "@/hooks/use-long-press";
import { useIsMobile } from "@/hooks/use-mobile";
import { removeChatTabAtom } from "@/atoms/tabs/tabs.atom";
import {
deleteThread,
fetchThreads,
@ -70,6 +72,7 @@ export function AllSharedChatsSidebarContent({
const params = useParams();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const removeChatTab = useSetAtom(removeChatTabAtom);
const currentChatId = Array.isArray(params.chat_id)
? Number(params.chat_id[0])
@ -158,6 +161,7 @@ export function AllSharedChatsSidebarContent({
setDeletingThreadId(threadId);
try {
await deleteThread(threadId);
removeChatTab(threadId);
toast.success(t("chat_deleted") || "Chat deleted successfully");
queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] });
queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] });
@ -176,7 +180,7 @@ export function AllSharedChatsSidebarContent({
setDeletingThreadId(null);
}
},
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange]
[queryClient, searchSpaceId, t, currentChatId, router, onOpenChange, removeChatTab]
);
const handleToggleArchive = useCallback(