"use client" import { memo, useCallback, useEffect, useMemo, useState } from "react" import { useTranslations } from "next-intl" import { toast } from "sonner" import { Check, ChevronsUpDown, Folder, GitBranch, Loader2 } from "lucide-react" import { useAppWorkspace } from "@/contexts/app-workspace-context" import { useTabContext } from "@/contexts/tab-context" import { useTaskContext } from "@/contexts/task-context" import { gitListAllBranches, gitCheckout } from "@/lib/api" import type { GitBranchList } from "@/lib/types" import { Button } from "@/components/ui/button" import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { cn } from "@/lib/utils" interface ConversationContextBarProps { tabId?: string | null } export const ConversationContextBar = memo(function ConversationContextBar({ tabId, }: ConversationContextBarProps = {}) { const t = useTranslations("Folder.conversationContextBar") const tBd = useTranslations("Folder.branchDropdown") const { tabs, activeTabId, setTabFolder } = useTabContext() const { folders, allFolders, branches, setBranch, addFolderToWorkspaceById, refreshFolder, } = useAppWorkspace() const { addTask, updateTask } = useTaskContext() const ownTab = useMemo(() => { const lookupId = tabId ?? activeTabId return tabs.find((x) => x.id === lookupId) ?? null }, [tabs, tabId, activeTabId]) const ownFolder = useMemo( () => ownTab ? (allFolders.find((f) => f.id === ownTab.folderId) ?? null) : null, [ownTab, allFolders] ) if (!ownTab || !ownFolder) return null const isNewConversation = ownTab.conversationId == null const currentBranch = branches.get(ownFolder.id) ?? ownFolder.git_branch ?? null return (
{ const target = allFolders.find((f) => f.id === folderId) if (!target) return const isOpen = folders.some((f) => f.id === folderId) try { const detail = isOpen ? target : await addFolderToWorkspaceById(folderId) setTabFolder(ownTab.id, detail.id, detail.path) toast.success(t("toasts.folderChanged", { name: detail.name })) } catch (err) { console.error( "[ConversationContextBar] switch folder failed:", err ) toast.error(t("toasts.openFolderFailed")) } }} labelEmpty={t("noFolders")} labelSearch={t("searchFolder")} /> { const taskId = `checkout-${ownFolder.id}-${Date.now()}` addTask(taskId, tBd("tasks.checkoutTo", { branchName })) updateTask(taskId, { status: "running" }) try { await gitCheckout(ownFolder.path, branchName) setBranch(ownFolder.id, branchName) await refreshFolder(ownFolder.id) updateTask(taskId, { status: "completed" }) } catch (err) { const msg = err instanceof Error ? err.message : String(err) updateTask(taskId, { status: "failed", error: msg }) toast.error(msg) } }} />
) }) ConversationContextBar.displayName = "ConversationContextBar" // ============================================================================ // FolderPicker // ============================================================================ interface FolderPickerProps { folders: { id: number; name: string; path: string }[] currentFolderId: number currentFolderName: string editable: boolean onSelect: (folderId: number) => void | Promise labelEmpty: string labelSearch: string } const FolderPicker = memo(function FolderPicker({ folders, currentFolderId, currentFolderName, editable, onSelect, labelEmpty, labelSearch, }: FolderPickerProps) { const [open, setOpen] = useState(false) const trigger = ( ) if (!editable) { return ( {trigger} {currentFolderName} ) } return ( {trigger} {labelEmpty} {folders.map((f) => ( { setOpen(false) void onSelect(f.id) }} >
{f.name} {f.path}
{f.id === currentFolderId && ( )}
))}
) }) // ============================================================================ // BranchPicker // ============================================================================ interface BranchPickerProps { folderId: number folderPath: string currentBranch: string | null onCheckout: (branchName: string) => Promise } const BranchPicker = memo(function BranchPicker({ folderId, folderPath, currentBranch, onCheckout, }: BranchPickerProps) { const t = useTranslations("Folder.conversationContextBar") const tBd = useTranslations("Folder.branchDropdown") const [open, setOpen] = useState(false) const [branchList, setBranchList] = useState(null) const [loading, setLoading] = useState(false) const loadBranches = useCallback(async () => { setLoading(true) try { const list = await gitListAllBranches(folderPath) setBranchList(list) } catch (err) { console.error("[BranchPicker] list failed:", err) setBranchList({ local: [], remote: [], worktree_branches: [] }) } finally { setLoading(false) } }, [folderPath]) useEffect(() => { if (open) void loadBranches() }, [open, loadBranches]) // Reset branches cache when folder changes useEffect(() => { setBranchList(null) }, [folderId]) return ( {loading ? (
) : ( <> {t("noBranches")} {branchList && branchList.local.length > 0 && ( {branchList.local.map((b) => ( { setOpen(false) if (b !== currentBranch) void onCheckout(b) }} > {b} {b === currentBranch && ( )} ))} )} {branchList && branchList.remote.length > 0 && ( {branchList.remote.map((b) => ( { setOpen(false) void onCheckout(b) }} > {b} ))} )} )}
) })