"use client" import { useMemo, useState } from "react" import { ChevronRight, FileIcon } from "lucide-react" import { useTranslations } from "next-intl" import { useFolderContext } from "@/contexts/folder-context" import { useTabContext } from "@/contexts/tab-context" import { useConversationRuntime } from "@/contexts/conversation-runtime-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { useConversationDetail } from "@/hooks/use-conversation-detail" import { extractSessionFilesGrouped } from "@/lib/session-files" import { CommitFileAdditions, CommitFileDeletions, } from "@/components/ai-elements/commit" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { cn } from "@/lib/utils" function isRemovedFileDiff(diff: string | null): boolean { if (!diff) return false return ( /^\*\*\* Delete File:\s+/m.test(diff) || /^deleted file mode\b/m.test(diff) || /^\+\+\+\s+\/dev\/null$/m.test(diff) ) } function normalizeSlashPath(path: string): string { return path.replace(/\\/g, "/") } function toFolderRelativePath(filePath: string, folderPath?: string): string { const normalizedFilePath = normalizeSlashPath(filePath) if (!folderPath) return normalizedFilePath const normalizedFolderPath = normalizeSlashPath(folderPath).replace( /\/+$/, "" ) if (!normalizedFolderPath) return normalizedFilePath const folderPrefix = `${normalizedFolderPath}/` if (normalizedFilePath.startsWith(folderPrefix)) { return normalizedFilePath.slice(folderPrefix.length) } return normalizedFilePath } function SessionFilesContent({ conversationId }: { conversationId: number }) { const t = useTranslations("Folder.sessionFiles") const { loading } = useConversationDetail(conversationId) const { getTimelineTurns } = useConversationRuntime() const { openSessionFileDiff } = useWorkspaceContext() const { folder } = useFolderContext() const [openGroups, setOpenGroups] = useState>({}) const turns = useMemo( () => getTimelineTurns(conversationId).map((item) => item.turn), [conversationId, getTimelineTurns] ) const groups = useMemo( () => (turns.length > 0 ? extractSessionFilesGrouped(turns) : []), [turns] ) const handleFileClick = ( filePath: string, diffContent: string | null, groupIndex: number, changeIndex: number ) => { openSessionFileDiff( filePath, diffContent ?? t("noDiffDataAvailable", { filePath }), `msg-${groupIndex + 1}-chg-${changeIndex + 1}` ) } if (loading && groups.length === 0) { return (

{t("loading")}

) } if (groups.length === 0) { return (

{t("noFileChangesInConversation")}

) } return (
{groups.map((group, groupIndex) => { const groupKey = `${group.userTurnId}-${group.timestamp}-${groupIndex}` const isOpen = openGroups[groupKey] ?? false const totalAdditions = group.files.reduce( (sum, f) => sum + f.additions, 0 ) const totalDeletions = group.files.reduce( (sum, f) => sum + f.deletions, 0 ) const uniqueFileCount = new Set( group.files.map((file) => file.path.replace(/\\/g, "/")) ).size return ( setOpenGroups((prev) => ({ ...prev, [groupKey]: open, })) } >
    {group.files.map((file, fileIndex) => { const normalizedDisplayPath = toFolderRelativePath( file.path, folder?.path ) const lastSlash = normalizedDisplayPath.lastIndexOf("/") const fileName = lastSlash >= 0 ? normalizedDisplayPath.slice(lastSlash + 1) : normalizedDisplayPath const isRemoved = isRemovedFileDiff(file.diff) return (
  • ) })}
) })}
) } export function SessionFilesTab() { const t = useTranslations("Folder.sessionFiles") const { tabs, activeTabId } = useTabContext() const activeTab = tabs.find((t) => t.id === activeTabId) const conversationId = activeTab?.runtimeConversationId ?? activeTab?.conversationId if (!activeTab) { return (

{t("openConversationToSeeChanges")}

) } if (!conversationId) { return (

{t("noFileChangesInConversation")}

) } return (
) }