diff --git a/CLAUDE.md b/CLAUDE.md index 6304af68e..e38f47dd1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,11 +12,11 @@ Maple is a **Tauri-based AI chat application** that runs on desktop (macOS, Linu ### Frontend - `src/app.tsx` - App entry, sets up all providers (OpenSecret, QueryClient, etc.) - `src/components/UnifiedChat.tsx` - **Main chat interface** (the logged-in experience) -- `src/state/LocalStateContext.tsx` - Global state (chats, models, billing status) +- `src/state/LocalStateContext.tsx` - Global UI, model, and billing state - `src/ai/OpenAIContext.tsx` - OpenAI API integration - `src/utils/platform.ts` - Platform detection (iOS/Android/macOS/desktop/web) - `src/billing/billingApi.ts` - Subscription and billing logic -- `src/routes/` - TanStack Router file-based routing (`_auth.*` routes require login) +- `src/routes/` - TanStack Router file-based routing ### Rust (src-tauri) - `src/lib.rs` - Tauri entry point, plugin setup, command handlers diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index 5a64ac458..338ff6727 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -56,24 +56,12 @@ import packageJson from "../../package.json"; import { SIDEBAR_ACCOUNT_MENU_WIDTH_CLASS, SIDEBAR_LAYOUT_STYLE } from "@/constants/layout"; function ConfirmDeleteDialog() { - const { clearHistory } = useLocalState(); const os = useOpenSecret(); const queryClient = useQueryClient(); const navigate = useNavigate(); async function handleDeleteHistory() { - // 1. Delete archived chats (KV) try { - await clearHistory(); - console.log("History (KV) cleared"); - } catch (error) { - console.error("Error clearing history:", error); - // Continue to delete server conversations even if this fails - } - - // 2. Delete server conversations (API) if any exist - try { - // Check if we have any conversations to delete const conversations = await os.listConversations({ limit: 1 }); if (conversations.data && conversations.data.length > 0) { await os.deleteConversations(); @@ -84,10 +72,13 @@ function ConfirmDeleteDialog() { } // Always refresh UI and navigate home - queryClient.invalidateQueries({ queryKey: ["chatHistory"] }); queryClient.invalidateQueries({ queryKey: ["conversations"] }); - queryClient.invalidateQueries({ queryKey: ["archivedChats"] }); + queryClient.invalidateQueries({ queryKey: ["pinnedConversations"] }); + queryClient.invalidateQueries({ queryKey: ["projectConversations"] }); + queryClient.invalidateQueries({ queryKey: ["conversationProjects"] }); + queryClient.invalidateQueries({ queryKey: ["conversationProject"] }); navigate({ to: "/" }); + window.dispatchEvent(new CustomEvent("newchat", { detail: { projectId: null } })); } return ( diff --git a/frontend/src/components/ChatHistoryList.tsx b/frontend/src/components/ChatHistoryList.tsx index 36793387c..169756ebb 100644 --- a/frontend/src/components/ChatHistoryList.tsx +++ b/frontend/src/components/ChatHistoryList.tsx @@ -70,13 +70,6 @@ interface ChatHistoryListProps { containerRef?: React.RefObject; } -interface ArchivedChat { - id: string; - title: string; - updated_at: number; - created_at: number; -} - export function ChatHistoryList({ currentChatId, searchQuery = "", @@ -105,7 +98,6 @@ export function ChatHistoryList({ const [selectedChat, setSelectedChat] = useState<{ id: string; title: string } | null>(null); const [selectedProject, setSelectedProject] = useState(null); const [expandedProjectId, setExpandedProjectId] = useState(selectedProjectId); - const [isArchivedExpanded, setIsArchivedExpanded] = useState(false); const longPressTimerRef = useRef | null>(null); // Pagination states @@ -509,30 +501,6 @@ export function ChatHistoryList({ }; }, [hasMoreConversations, isLoadingMore, loadMoreConversations]); - // Fetch archived chats from KV store - const { data: archivedChats } = useQuery({ - queryKey: ["archivedChats"], - queryFn: async () => { - if (!opensecret?.get) return []; - - try { - const historyListStr = await opensecret.get("history_list"); - if (!historyListStr) return []; - - const historyList = JSON.parse(historyListStr) as ArchivedChat[]; - if (!Array.isArray(historyList)) return []; - - // Sort by updated_at descending (most recent first) - return historyList.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0)); - } catch (error) { - console.error("Error loading archived chats:", error); - return []; - } - }, - enabled: !!opensecret?.get, - retry: false - }); - const { data: conversationProjects = [] } = useQuery({ queryKey: ["conversationProjects", userId], queryFn: () => listAllConversationProjects(opensecret), @@ -639,21 +607,6 @@ export function ChatHistoryList({ ); }, [conversations, getConversationTitle, normalizedQuery]); - // Filter archived chats based on search query - const filteredArchivedChats = useMemo(() => { - if (!archivedChats) return []; - if (!normalizedQuery) return archivedChats; - - return archivedChats.filter((chat) => chat.title.toLowerCase().includes(normalizedQuery)); - }, [archivedChats, normalizedQuery]); - - // Auto-expand archived section when searching with results - useEffect(() => { - if (normalizedQuery && filteredArchivedChats.length > 0) { - setIsArchivedExpanded(true); - } - }, [normalizedQuery, filteredArchivedChats.length]); - const dispatchConversationMetadataUpdated = useCallback( (conversationId: string, updates: Record) => { window.dispatchEvent( @@ -668,48 +621,22 @@ export function ChatHistoryList({ // Handle conversation deletion via API const handleDeleteConversation = useCallback( async (conversationId: string) => { - const isArchived = archivedChats?.some((chat) => chat.id === conversationId); - - if (isArchived) { - if (!localState?.deleteChat) return; - try { - await localState.deleteChat(conversationId); - await queryClient.invalidateQueries({ queryKey: ["archivedChats"] }); + try { + await opensecret.deleteConversation(conversationId); + setConversations((prev) => prev.filter((conv) => conv.id !== conversationId)); + await invalidateConversationData(); - if (conversationId === currentChatId) { - router.navigate({ to: "/" }); - setSelectedProjectId(null); - } - } catch (error) { - console.error("Error deleting archived chat:", error); - } - } else { - try { - await opensecret.deleteConversation(conversationId); - setConversations((prev) => prev.filter((conv) => conv.id !== conversationId)); - await invalidateConversationData(); - - if (conversationId === currentChatId) { - const params = new URLSearchParams(window.location.search); - params.delete("conversation_id"); - window.history.replaceState({}, "", params.toString() ? `/?${params}` : "/"); - window.dispatchEvent(new Event("newchat")); - } - } catch (error) { - console.error("Error deleting conversation:", error); + if (conversationId === currentChatId) { + const params = new URLSearchParams(window.location.search); + params.delete("conversation_id"); + window.history.replaceState({}, "", params.toString() ? `/?${params}` : "/"); + window.dispatchEvent(new Event("newchat")); } + } catch (error) { + console.error("Error deleting conversation:", error); } }, - [ - archivedChats, - currentChatId, - invalidateConversationData, - localState, - opensecret, - queryClient, - router, - setSelectedProjectId - ] + [currentChatId, invalidateConversationData, opensecret] ); const MAX_SELECTION = 20; @@ -739,38 +666,18 @@ export function ChatHistoryList({ setIsBulkDeleting(true); try { const idsToDelete = Array.from(selectedIds); + const deletedIds = new Set(); - // Separate archived chats from API conversations - const archivedIds = idsToDelete.filter((id) => archivedChats?.some((chat) => chat.id === id)); - const conversationIds = idsToDelete.filter( - (id) => !archivedChats?.some((chat) => chat.id === id) - ); - - // Delete API conversations using batch delete - if (conversationIds.length > 0 && opensecret) { - const result = await opensecret.batchDeleteConversations(conversationIds); + if (opensecret) { + const result = await opensecret.batchDeleteConversations(idsToDelete); - const deletedIds = new Set( - result.data.filter((item) => item.deleted).map((item) => item.id) - ); + result.data.filter((item) => item.deleted).forEach((item) => deletedIds.add(item.id)); setConversations((prev) => prev.filter((conv) => !deletedIds.has(conv.id))); await invalidateConversationData(); } - // Delete archived chats individually - for (const id of archivedIds) { - if (localState?.deleteChat) { - await localState.deleteChat(id); - } - } - - // Refresh archived chats if any were deleted - if (archivedIds.length > 0) { - queryClient.invalidateQueries({ queryKey: ["archivedChats"] }); - } - // If current chat was deleted, navigate to home - if (selectedIds.has(currentChatId || "")) { + if (currentChatId && deletedIds.has(currentChatId)) { const params = new URLSearchParams(window.location.search); params.delete("conversation_id"); window.history.replaceState({}, "", params.toString() ? `/?${params}` : "/"); @@ -788,11 +695,8 @@ export function ChatHistoryList({ } }, [ selectedIds, - archivedChats, opensecret, invalidateConversationData, - localState, - queryClient, currentChatId, onSelectionChange, onExitSelectionMode @@ -1017,11 +921,6 @@ export function ChatHistoryList({ [getConversationTitle] ); - const handleOpenRenameDialogArchived = useCallback((chat: ArchivedChat) => { - setSelectedChat({ id: chat.id, title: chat.title }); - setIsRenameDialogOpen(true); - }, []); - const handleOpenDeleteDialog = useCallback( (conv: Conversation) => { setSelectedChat({ id: conv.id, title: getConversationTitle(conv) }); @@ -1030,11 +929,6 @@ export function ChatHistoryList({ [getConversationTitle] ); - const handleOpenDeleteDialogArchived = useCallback((chat: ArchivedChat) => { - setSelectedChat({ id: chat.id, title: chat.title }); - setIsDeleteDialogOpen(true); - }, []); - const handleOpenCreateProjectDialog = useCallback(() => { setSelectedProject(null); setProjectDialogMode("create"); @@ -1055,45 +949,25 @@ export function ChatHistoryList({ // Handle conversation renaming via API const handleRenameConversation = useCallback( async (conversationId: string, newTitle: string) => { - const isArchived = archivedChats?.some((chat) => chat.id === conversationId); - - if (isArchived) { - if (!localState?.renameChat) return; - try { - await localState.renameChat(conversationId, newTitle); - await queryClient.invalidateQueries({ queryKey: ["archivedChats"] }); - } catch (error) { - console.error("Error renaming archived chat:", error); - throw error; - } - } else { - try { - await opensecret.updateConversation(conversationId, { title: newTitle }); - setConversations((prev) => - prev.map((conv) => - conv.id === conversationId - ? { ...conv, metadata: { ...(conv.metadata ?? {}), title: newTitle } } - : conv - ) - ); - await invalidateConversationData(); - dispatchConversationMetadataUpdated(conversationId, { - metadata: { title: newTitle } - }); - } catch (error) { - console.error("Error renaming conversation:", error); - throw error; - } + try { + await opensecret.updateConversation(conversationId, { title: newTitle }); + setConversations((prev) => + prev.map((conv) => + conv.id === conversationId + ? { ...conv, metadata: { ...(conv.metadata ?? {}), title: newTitle } } + : conv + ) + ); + await invalidateConversationData(); + dispatchConversationMetadataUpdated(conversationId, { + metadata: { title: newTitle } + }); + } catch (error) { + console.error("Error renaming conversation:", error); + throw error; } }, - [ - archivedChats, - dispatchConversationMetadataUpdated, - invalidateConversationData, - localState, - opensecret, - queryClient - ] + [dispatchConversationMetadataUpdated, invalidateConversationData, opensecret] ); // Handle conversation selection @@ -1145,8 +1019,7 @@ export function ChatHistoryList({ filteredProjects.length === 0 && filteredExpandedProjectConversations.length === 0 && filteredPinnedConversations.length === 0 && - filteredRecentConversations.length === 0 && - filteredArchivedChats.length === 0 + filteredRecentConversations.length === 0 ) { return (
@@ -1450,92 +1323,6 @@ export function ChatHistoryList({
)} - {filteredArchivedChats && filteredArchivedChats.length > 0 && ( -
- - - {isArchivedExpanded && ( -
- {filteredArchivedChats.map((chat) => { - const isActive = chat.id === currentChatId; - const archivedTitlePaddingClass = "pr-8"; - return ( -
-
{ - setSelectedProjectId(null); - router.navigate({ to: "/chat/$chatId", params: { chatId: chat.id } }); - }} - className={`relative ${ROW_CONTENT_Z} min-w-0 flex-1 cursor-pointer py-1 pl-0 pr-2 ${ - isActive - ? "font-bold text-foreground" - : "text-foreground/95 group-hover:text-foreground" - }`} - > -
-
- {chat.title} -
-
-
- {new Date(chat.updated_at || chat.created_at).toLocaleDateString()} -
-
-
- -
- ); - })} -
- )} -
- )} - {!trimmedQuery && hasMoreConversations ? (