From f0d530e1cb490197fa6e621550d557669816dae1 Mon Sep 17 00:00:00 2001 From: xintaofei Date: Tue, 21 Apr 2026 17:07:40 +0800 Subject: [PATCH] refactor(sidebar): streamline conversation list with completed filter and expand toggle Replace flat/grouped view toggle with a show-completed filter dropdown, add expand/collapse-all control, extract status icon component, and simplify the sidebar header. --- .../sidebar-conversation-card.tsx | 178 ++++-- .../sidebar-conversation-list.tsx | 579 ++++++------------ .../conversations/sidebar-status-icon.tsx | 180 ++++++ src/components/layout/sidebar.tsx | 245 +++----- src/i18n/messages/ar.json | 8 +- src/i18n/messages/de.json | 8 +- src/i18n/messages/en.json | 8 +- src/i18n/messages/es.json | 8 +- src/i18n/messages/fr.json | 8 +- src/i18n/messages/ja.json | 8 +- src/i18n/messages/ko.json | 8 +- src/i18n/messages/pt.json | 8 +- src/i18n/messages/zh-CN.json | 8 +- src/i18n/messages/zh-TW.json | 8 +- src/lib/sidebar-view-mode-storage.ts | 44 +- 15 files changed, 650 insertions(+), 656 deletions(-) create mode 100644 src/components/conversations/sidebar-status-icon.tsx diff --git a/src/components/conversations/sidebar-conversation-card.tsx b/src/components/conversations/sidebar-conversation-card.tsx index d356266..0779223 100644 --- a/src/components/conversations/sidebar-conversation-card.tsx +++ b/src/components/conversations/sidebar-conversation-card.tsx @@ -1,14 +1,22 @@ "use client" -import { memo, useState, useCallback, useMemo } from "react" -import { formatDistanceToNow } from "date-fns" -import { enUS, zhCN, zhTW } from "date-fns/locale" -import { GitBranch, Pencil, Trash2, Circle, Download, Plus } from "lucide-react" -import { useLocale, useTranslations } from "next-intl" +import { memo, useState, useCallback } from "react" +import { + Pencil, + Trash2, + Circle, + CircleAlert, + CircleCheck, + CircleDashed, + CircleX, + Download, + Plus, + type LucideIcon, +} from "lucide-react" +import { useTranslations } from "next-intl" import type { DbConversationSummary, ConversationStatus } from "@/lib/types" -import { STATUS_ORDER, STATUS_COLORS } from "@/lib/types" +import { STATUS_ORDER } from "@/lib/types" import { cn } from "@/lib/utils" -import { AgentIcon } from "@/components/agent-icon" import { ContextMenu, ContextMenuTrigger, @@ -38,10 +46,29 @@ import { } from "@/components/ui/alert-dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" +import { + SidebarStatusIcon, + conversationStatusToBead, +} from "./sidebar-status-icon" + +const STATUS_ICONS: Record = { + in_progress: CircleDashed, + pending_review: CircleAlert, + completed: CircleCheck, + cancelled: CircleX, +} + +const STATUS_ICON_COLORS: Record = { + in_progress: "text-blue-500", + pending_review: "text-orange-500", + completed: "text-green-500", + cancelled: "text-red-500", +} interface SidebarConversationCardProps { conversation: DbConversationSummary isSelected: boolean + timeLabel?: string onSelect: (id: number, agentType: string) => void onDoubleClick?: (id: number, agentType: string) => void onRename: (id: number, newTitle: string) => Promise @@ -55,6 +82,7 @@ interface SidebarConversationCardProps { export const SidebarConversationCard = memo(function SidebarConversationCard({ conversation, isSelected, + timeLabel, onSelect, onDoubleClick, onRename, @@ -65,23 +93,12 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({ importing, }: SidebarConversationCardProps) { const t = useTranslations("Folder.conversationCard") + const tSidebar = useTranslations("Folder.sidebar") const tStatus = useTranslations("Folder.statusLabels") - const locale = useLocale() - const dateFnsLocale = - locale === "zh-CN" ? zhCN : locale === "zh-TW" ? zhTW : enUS const [renameOpen, setRenameOpen] = useState(false) const [deleteOpen, setDeleteOpen] = useState(false) const [renameValue, setRenameValue] = useState("") - const timeAgo = useMemo( - () => - formatDistanceToNow(new Date(conversation.updated_at), { - addSuffix: true, - locale: dateFnsLocale, - }), - [conversation.updated_at, dateFnsLocale] - ) - const handleClick = useCallback(() => { onSelect(conversation.id, conversation.agent_type) }, [onSelect, conversation.id, conversation.agent_type]) @@ -108,40 +125,83 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({ setDeleteOpen(false) }, [conversation.id, conversation.agent_type, onDelete]) + const status = conversation.status as ConversationStatus + const beadStatus = conversationStatusToBead(conversation.status) + const isRunning = status === "in_progress" + const isFailed = status === "cancelled" + return ( <> - + ) : isFailed ? ( + + {tSidebar("statusFailedBadge")} + + ) : timeLabel ? ( + + {timeLabel} + + ) : null} + + {onNewConversation && ( @@ -165,20 +225,20 @@ export const SidebarConversationCard = memo(function SidebarConversationCard({ {STATUS_ORDER.filter((s) => s !== conversation.status).map( - (s) => ( - onStatusChange(conversation.id, s)} - > - - {tStatus(s)} - - ) + (s) => { + const StatusIcon = STATUS_ICONS[s] + return ( + onStatusChange(conversation.id, s)} + > + + {tStatus(s)} + + ) + } )} diff --git a/src/components/conversations/sidebar-conversation-list.tsx b/src/components/conversations/sidebar-conversation-list.tsx index 0203c74..2e24fa8 100644 --- a/src/components/conversations/sidebar-conversation-list.tsx +++ b/src/components/conversations/sidebar-conversation-list.tsx @@ -13,18 +13,12 @@ import { import { useTranslations } from "next-intl" import { toast } from "sonner" import { Virtualizer, type VirtualizerHandle } from "virtua" -import { - CheckCheck, - ChevronRight, - Download, - Loader2, - Plus, - XCircle, -} from "lucide-react" +import { ChevronRight, Download, Loader2, Plus, XCircle } from "lucide-react" import { useActiveFolder } from "@/contexts/active-folder-context" import { useAppWorkspace } from "@/contexts/app-workspace-context" import { useTabContext } from "@/contexts/tab-context" import { useTaskContext } from "@/contexts/task-context" +import { useZoomLevel } from "@/hooks/use-appearance" import { importLocalConversations, updateConversationTitle, @@ -32,13 +26,12 @@ import { deleteConversation, } from "@/lib/api" import type { ConversationStatus, DbConversationSummary } from "@/lib/types" -import { STATUS_ORDER, STATUS_COLORS } from "@/lib/types" import { loadFolderExpanded, saveFolderExpanded, - type SidebarViewMode, } from "@/lib/sidebar-view-mode-storage" import { SidebarConversationCard } from "./sidebar-conversation-card" +import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { ScrollArea } from "@/components/ui/scroll-area" @@ -81,6 +74,23 @@ function compareByUpdatedAtDesc( return right.id - left.id } +function formatRelative(iso: string): string { + const ts = parseTimestamp(iso) + if (!ts) return "" + const diff = Math.max(0, Date.now() - ts) + const m = Math.floor(diff / 60000) + if (m < 1) return "now" + if (m < 60) return `${m}m` + const h = Math.floor(m / 60) + if (h < 24) return `${h}h` + const d = Math.floor(h / 24) + if (d < 30) return `${d}d` + const mo = Math.floor(d / 30) + if (mo < 12) return `${mo}mo` + const y = Math.floor(mo / 12) + return `${y}y` +} + type FlatItem = | { type: "folder_header" @@ -90,15 +100,9 @@ type FlatItem = count: number expanded: boolean } - | { - type: "status_header" - status: ConversationStatus - count: number - parentFolderId?: number - } | { type: "conversation"; conversation: DbConversationSummary } -const CARD_HEIGHT = 62 +const CARD_HEIGHT_REM = 2 const FolderHeader = memo(function FolderHeader({ folderId, @@ -128,31 +132,49 @@ const FolderHeader = memo(function FolderHeader({ return ( - +
+ + {folderName} + + {branch && ( + + {branch} + + )} +
+ + {count} + + +
onFocus(folderId)}> @@ -174,96 +196,6 @@ const FolderHeader = memo(function FolderHeader({ ) }) -const StatusHeader = memo(function StatusHeader({ - status, - count, - isOpen, - onToggle, - tStatus, -}: { - status: ConversationStatus - count: number - isOpen: boolean - onToggle: (status: ConversationStatus) => void - tStatus: ReturnType -}) { - return ( - - ) -}) - -const PendingReviewHeader = memo(function PendingReviewHeader({ - count, - isOpen, - onToggle, - reviewConversationCount, - completingReview, - onCompleteReview, - tStatus, - t, -}: { - count: number - isOpen: boolean - onToggle: (status: ConversationStatus) => void - reviewConversationCount: number - completingReview: boolean - onCompleteReview: () => void - tStatus: ReturnType - t: ReturnType -}) { - return ( - - - - - - - - {t("completeAllSessions")} - - - - ) -}) - export interface SidebarConversationListHandle { scrollToActive: () => void expandAll: () => void @@ -272,20 +204,19 @@ export interface SidebarConversationListHandle { } export interface SidebarConversationListProps { - viewMode?: SidebarViewMode - searchQuery?: string + showCompleted?: boolean } export function SidebarConversationList({ ref, - viewMode = "flat", - searchQuery = "", + showCompleted = true, }: SidebarConversationListProps & { ref?: Ref }) { const t = useTranslations("Folder.sidebar") - const tStatus = useTranslations("Folder.statusLabels") const tCommon = useTranslations("Folder.common") + const { zoomLevel } = useZoomLevel() + const cardHeightPx = (CARD_HEIGHT_REM * 16 * zoomLevel) / 100 const { allFolders, conversations, @@ -325,22 +256,13 @@ export function SidebarConversationList({ }, [tabs, activeTabId]) const [importing, setImporting] = useState(false) - const [completeReviewOpen, setCompleteReviewOpen] = useState(false) - const [completingReview, setCompletingReview] = useState(false) - const [groupExpanded, setGroupExpanded] = useState< - Record - >({ - in_progress: true, - pending_review: true, - completed: false, - cancelled: false, - }) const [folderExpanded, setFolderExpanded] = useState>( {} ) const [highlightedFolder, setHighlightedFolder] = useState( null ) + const [scrollOffset, setScrollOffset] = useState(0) const [removeConfirm, setRemoveConfirm] = useState<{ folderId: number folderName: string @@ -357,53 +279,25 @@ export function SidebarConversationList({ const virtualizerRef = useRef(null) const highlightTimerRef = useRef(null) - const normalizedSearch = searchQuery.trim().toLowerCase() const filteredConversations = useMemo(() => { - if (!normalizedSearch) return conversations - return conversations.filter((c) => { - const title = (c.title ?? "").toLowerCase() - return title.includes(normalizedSearch) - }) - }, [conversations, normalizedSearch]) + if (showCompleted) return conversations + return conversations.filter( + (c) => c.status !== "completed" && c.status !== "cancelled" + ) + }, [conversations, showCompleted]) - const byStatus = useMemo(() => { - const map = new Map() + const byFolder = useMemo(() => { + const map = new Map() for (const conv of filteredConversations) { - const status = conv.status as ConversationStatus - const list = map.get(status) + const list = map.get(conv.folder_id) if (list) list.push(conv) - else map.set(status, [conv]) + else map.set(conv.folder_id, [conv]) } for (const list of map.values()) list.sort(compareByUpdatedAtDesc) return map }, [filteredConversations]) - const byFolder = useMemo(() => { - const map = new Map< - number, - Map - >() - for (const conv of filteredConversations) { - const folderId = conv.folder_id - let inner = map.get(folderId) - if (!inner) { - inner = new Map() - map.set(folderId, inner) - } - const status = conv.status as ConversationStatus - const list = inner.get(status) - if (list) list.push(conv) - else inner.set(status, [conv]) - } - for (const inner of map.values()) { - for (const list of inner.values()) list.sort(compareByUpdatedAtDesc) - } - return map - }, [filteredConversations]) - const orderedFolderIds = useMemo(() => { - // Show every folder in the workspace DB, even ones without conversations. - // Folders that only have orphan conversations still appear via byFolder. const seen = new Set() const ids: number[] = [] for (const f of allFolders) { @@ -423,91 +317,83 @@ export function SidebarConversationList({ const flatItems = useMemo(() => { const items: FlatItem[] = [] - if (viewMode === "grouped") { - for (const folderId of orderedFolderIds) { - const inner = byFolder.get(folderId) - const totalCount = inner - ? Array.from(inner.values()).reduce( - (sum, list) => sum + list.length, - 0 - ) - : 0 - const folderName = folderIndex.get(folderId)?.name ?? String(folderId) - const branch = branches.get(folderId) ?? null - const expanded = folderExpanded[folderId] ?? true - items.push({ - type: "folder_header", - folderId, - folderName, - branch, - count: totalCount, - expanded, - }) - if (!expanded || !inner) continue - for (const status of STATUS_ORDER) { - const list = inner.get(status) - if (!list || list.length === 0) continue - items.push({ - type: "status_header", - status, - count: list.length, - parentFolderId: folderId, - }) - if (groupExpanded[status]) { - for (const conv of list) { - items.push({ type: "conversation", conversation: conv }) - } - } - } - } - } else { - for (const status of STATUS_ORDER) { - const list = byStatus.get(status) - if (!list || list.length === 0) continue - items.push({ type: "status_header", status, count: list.length }) - if (groupExpanded[status]) { - for (const conv of list) { - items.push({ type: "conversation", conversation: conv }) - } - } + for (const folderId of orderedFolderIds) { + const list = byFolder.get(folderId) ?? [] + const folderName = folderIndex.get(folderId)?.name ?? String(folderId) + const branch = branches.get(folderId) ?? null + const expanded = folderExpanded[folderId] ?? true + items.push({ + type: "folder_header", + folderId, + folderName, + branch, + count: list.length, + expanded, + }) + if (!expanded) continue + for (const conv of list) { + items.push({ type: "conversation", conversation: conv }) } } return items - }, [ - viewMode, - orderedFolderIds, - byFolder, - folderIndex, - branches, - folderExpanded, - byStatus, - groupExpanded, - ]) + }, [orderedFolderIds, byFolder, folderIndex, branches, folderExpanded]) - const reviewConversations = useMemo( - () => byStatus.get("pending_review") ?? [], - [byStatus] - ) - const reviewConversationCount = reviewConversations.length + const stickyState = useMemo<{ + folder: Extract | null + pushOffset: number + }>(() => { + const vr = virtualizerRef.current + const startIdx = vr ? vr.findItemIndex(scrollOffset) : 0 + let folderIdx = -1 + for (let i = Math.min(startIdx, flatItems.length - 1); i >= 0; i--) { + if (flatItems[i]?.type === "folder_header") { + folderIdx = i + break + } + } + if (folderIdx < 0) { + return { folder: null, pushOffset: 0 } + } + const folder = flatItems[folderIdx] as Extract< + FlatItem, + { type: "folder_header" } + > + let pushOffset = 0 + if (vr) { + const stickyHeight = vr.getItemSize(folderIdx) || cardHeightPx + for (let i = folderIdx + 1; i < flatItems.length; i++) { + if (flatItems[i].type === "folder_header") { + const nextRelativeY = vr.getItemOffset(i) - scrollOffset + if (nextRelativeY < stickyHeight) { + pushOffset = Math.min(0, nextRelativeY - stickyHeight) + } + break + } + } + } + return { folder, pushOffset } + }, [scrollOffset, flatItems, cardHeightPx]) + + const stickyFolderItem = stickyState.folder useImperativeHandle(ref, () => ({ scrollToActive() { scrollToActiveRef.current() }, expandAll() { - setGroupExpanded({ - in_progress: true, - pending_review: true, - completed: true, - cancelled: true, + setFolderExpanded((prev) => { + const next: Record = { ...prev } + for (const id of orderedFolderIds) next[id] = true + saveFolderExpanded(next) + return next }) }, collapseAll() { - setGroupExpanded({ - in_progress: false, - pending_review: false, - completed: false, - cancelled: false, + setFolderExpanded((prev) => { + const next: Record = { ...prev } + for (const id of orderedFolderIds) next[id] = false + saveFolderExpanded(next) + return next }) }, revealFolder(folderId: number) { @@ -556,13 +442,7 @@ export function SidebarConversationList({ (c) => c.id === targetId && c.agent_type === targetAgent ) if (!conv) return - const status = conv.status as ConversationStatus - if (!groupExpanded[status]) { - setGroupExpanded((prev) => ({ ...prev, [status]: true })) - pendingScrollRef.current = true - return - } - if (viewMode === "grouped" && !(folderExpanded[conv.folder_id] ?? true)) { + if (!(folderExpanded[conv.folder_id] ?? true)) { setFolderExpanded((prev) => { const next = { ...prev, [conv.folder_id]: true } saveFolderExpanded(next) @@ -589,18 +469,7 @@ export function SidebarConversationList({ pendingScrollRef.current = false scrollToActiveRef.current() } - }, [ - selectedConversation, - flatItems, - conversations, - groupExpanded, - folderExpanded, - viewMode, - ]) - - const toggleGroup = useCallback((status: ConversationStatus) => { - setGroupExpanded((prev) => ({ ...prev, [status]: !prev[status] })) - }, []) + }, [selectedConversation, flatItems, conversations, folderExpanded]) const toggleFolder = useCallback((folderId: number) => { setFolderExpanded((prev) => { @@ -655,11 +524,6 @@ export function SidebarConversationList({ } }, [removeConfirm, closeTabsByFolder, removeFolderFromWorkspace, t]) - const handleOpenCompleteReview = useCallback( - () => setCompleteReviewOpen(true), - [] - ) - const handleSelect = useCallback( (id: number, agentType: string) => { const conv = conversations.find( @@ -761,40 +625,8 @@ export function SidebarConversationList({ } }, [importing, activeFolder, addTask, updateTask, refreshConversations, t]) - const handleCompleteAllReview = useCallback(async () => { - if (completingReview || reviewConversationCount === 0) return - setCompletingReview(true) - try { - for (const conversation of reviewConversations) { - updateConversationLocal(conversation.id, { status: "completed" }) - } - await Promise.all( - reviewConversations.map((conversation) => - updateConversationStatus(conversation.id, "completed") - ) - ) - toast.success( - t("toasts.reviewCompleted", { count: reviewConversationCount }) - ) - setCompleteReviewOpen(false) - } catch (e) { - const msg = e instanceof Error ? e.message : String(e) - toast.error(t("toasts.completeReviewFailed", { message: msg })) - refreshConversations() - } finally { - setCompletingReview(false) - } - }, [ - completingReview, - reviewConversationCount, - reviewConversations, - refreshConversations, - updateConversationLocal, - t, - ]) - - const emptyAfterSearch = - filteredConversations.length === 0 && normalizedSearch.length > 0 + const emptyAfterFilter = + filteredConversations.length === 0 && conversations.length > 0 return (
@@ -856,7 +688,7 @@ export function SidebarConversationList({ - ) : emptyAfterSearch ? ( + ) : emptyAfterFilter ? (

{t("noMatchingConversations")} @@ -865,12 +697,50 @@ export function SidebarConversationList({ ) : ( -

+
+ {stickyFolderItem && ( +
+
+
+ +
+
+ )} - - {flatItems.map((item, index) => { + + {flatItems.map((item) => { if (item.type === "folder_header") { return ( ) } - const indented = - viewMode === "grouped" && - (item.type === "status_header" - ? item.parentFolderId != null - : true) - if (item.type === "status_header") { - const key = `status-${item.parentFolderId ?? "root"}-${item.status}-${index}` - const headerNode = - item.status === "pending_review" ? ( - - ) : ( - - ) - return indented ? ( -
- {headerNode} -
- ) : ( - headerNode - ) - } const conv = item.conversation - const cardNode = ( + return ( ) - return indented ? ( -
- {cardNode} -
- ) : ( -
{cardNode}
- ) })}
@@ -976,34 +803,6 @@ export function SidebarConversationList({ )} - - !completingReview && setCompleteReviewOpen(open) - } - > - - - {t("completeAllReviewTitle")} - - {t("completeAllReviewDescription", { - count: reviewConversationCount, - })} - - - - - {tCommon("cancel")} - - - {completingReview ? t("completing") : tCommon("confirm")} - - - - + + + + + + + + + +
+ ) + } + + if (status === "failed") { + return ( +
+ + + + +
+ ) + } + + if (status === "active") { + return ( +
+ + + + +
+ ) + } + + return ( +
+ ) +} + +export function conversationStatusToBead(status: string): SidebarBeadStatus { + switch (status) { + case "in_progress": + return "running" + case "pending_review": + return "active" + case "cancelled": + return "failed" + case "completed": + default: + return "done" + } +} diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index ad60239..578cceb 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -1,29 +1,26 @@ "use client" -import { useCallback, useEffect, useRef, useState } from "react" +import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { ChevronsDownUp, ChevronsUpDown, Crosshair, - FolderPlus, - FolderTree, - Plus, - Rows3, - Search, - X, + EllipsisVertical, } from "lucide-react" import { useTranslations } from "next-intl" -import { toast } from "sonner" -import { useActiveFolder } from "@/contexts/active-folder-context" -import { useAppWorkspace } from "@/contexts/app-workspace-context" -import { useTabContext } from "@/contexts/tab-context" import { useSidebarContext } from "@/contexts/sidebar-context" +import { useAppWorkspace } from "@/contexts/app-workspace-context" import { SidebarConversationList, type SidebarConversationListHandle, } from "@/components/conversations/sidebar-conversation-list" import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" import { Tooltip, TooltipContent, @@ -31,165 +28,76 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { useIsMobile } from "@/hooks/use-mobile" -import { isDesktop, openFileDialog } from "@/lib/platform" import { - loadSidebarViewMode, - saveSidebarViewMode, - type SidebarViewMode, + loadShowCompleted, + saveShowCompleted, } from "@/lib/sidebar-view-mode-storage" -import { cn } from "@/lib/utils" export function Sidebar() { const t = useTranslations("Folder.sidebar") - const { activeFolder } = useActiveFolder() - const { allFolders, conversations, openFolder } = useAppWorkspace() - const { openNewConversationTab } = useTabContext() const { isOpen, toggle } = useSidebarContext() + const { conversations } = useAppWorkspace() const isMobile = useIsMobile() const listRef = useRef(null) - const [viewMode, setViewMode] = useState("flat") - const [searchQuery, setSearchQuery] = useState("") + const [showCompleted, setShowCompleted] = useState(false) + const [allExpanded, setAllExpanded] = useState(true) + + const visibleCount = useMemo(() => { + if (showCompleted) return conversations.length + return conversations.filter( + (c) => c.status !== "completed" && c.status !== "cancelled" + ).length + }, [conversations, showCompleted]) useEffect(() => { // Hydrate from localStorage after mount to keep SSR/CSR markup consistent. // eslint-disable-next-line react-hooks/set-state-in-effect - setViewMode(loadSidebarViewMode()) + setShowCompleted(loadShowCompleted()) }, []) - const handleSetViewMode = useCallback((mode: SidebarViewMode) => { - setViewMode(mode) - saveSidebarViewMode(mode) + const handleSetShowCompleted = useCallback((value: boolean) => { + setShowCompleted(value) + saveShowCompleted(value) }, []) + const handleToggleExpandAll = useCallback(() => { + if (allExpanded) { + listRef.current?.collapseAll() + setAllExpanded(false) + } else { + listRef.current?.expandAll() + setAllExpanded(true) + } + }, [allExpanded]) + useEffect(() => { const onReveal = (e: Event) => { const detail = (e as CustomEvent<{ folderId: number }>).detail if (!detail) return - if (viewMode !== "grouped") { - setViewMode("grouped") - saveSidebarViewMode("grouped") - } listRef.current?.revealFolder(detail.folderId) } window.addEventListener("sidebar:reveal-folder", onReveal) return () => { window.removeEventListener("sidebar:reveal-folder", onReveal) } - }, [viewMode]) - - const handleNewConversation = useCallback(() => { - if (!activeFolder) return - openNewConversationTab(activeFolder.id, activeFolder.path) - }, [activeFolder, openNewConversationTab]) - - const handleOpenFolder = useCallback(async () => { - try { - if (!isDesktop()) { - toast.error(t("toasts.openFolderFailed")) - return - } - const result = await openFileDialog({ - directory: true, - multiple: false, - }) - if (!result) return - const selected = Array.isArray(result) ? result[0] : result - const detail = await openFolder(selected) - toast.success(t("toasts.folderOpened", { name: detail.name })) - } catch (err) { - console.error("[Sidebar] open folder failed:", err) - toast.error(t("toasts.openFolderFailed")) - } - }, [openFolder, t]) + }, []) if (!isOpen) return null return ( -