refactor(conversation-context-bar): slim folder and branch pickers to selection only

- Match popover corner styling of folder picker with branch picker
- Drop 'open folder from disk' entry from folder picker
- Drop 'new branch' entry and dialog from branch picker
This commit is contained in:
xintaofei
2026-04-22 19:30:58 +08:00
parent d26c0b91c9
commit 4440249678
11 changed files with 77 additions and 236 deletions

View File

@@ -3,23 +3,13 @@
import { memo, useCallback, useEffect, useMemo, useState } from "react" import { memo, useCallback, useEffect, useMemo, useState } from "react"
import { useTranslations } from "next-intl" import { useTranslations } from "next-intl"
import { toast } from "sonner" import { toast } from "sonner"
import { import { Check, ChevronsUpDown, Folder, GitBranch, Loader2 } from "lucide-react"
Check,
ChevronsUpDown,
Folder,
FolderOpen,
GitBranch,
Loader2,
Plus,
} from "lucide-react"
import { useAppWorkspace } from "@/contexts/app-workspace-context" import { useAppWorkspace } from "@/contexts/app-workspace-context"
import { useTabContext } from "@/contexts/tab-context" import { useTabContext } from "@/contexts/tab-context"
import { useTaskContext } from "@/contexts/task-context" import { useTaskContext } from "@/contexts/task-context"
import { gitListAllBranches, gitCheckout, gitNewBranch } from "@/lib/api" import { gitListAllBranches, gitCheckout } from "@/lib/api"
import { isDesktop, openFileDialog } from "@/lib/platform"
import type { GitBranchList } from "@/lib/types" import type { GitBranchList } from "@/lib/types"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
@@ -32,15 +22,7 @@ import {
CommandInput, CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
CommandSeparator,
} from "@/components/ui/command" } from "@/components/ui/command"
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -64,7 +46,6 @@ export const ConversationContextBar = memo(function ConversationContextBar({
allFolders, allFolders,
branches, branches,
setBranch, setBranch,
openFolder,
addFolderToWorkspaceById, addFolderToWorkspaceById,
refreshFolder, refreshFolder,
} = useAppWorkspace() } = useAppWorkspace()
@@ -115,25 +96,6 @@ export const ConversationContextBar = memo(function ConversationContextBar({
toast.error(t("toasts.openFolderFailed")) toast.error(t("toasts.openFolderFailed"))
} }
}} }}
onOpenNewFolder={async () => {
try {
if (isDesktop()) {
const result = await openFileDialog({
directory: true,
multiple: false,
})
if (!result) return
const selected = Array.isArray(result) ? result[0] : result
const detail = await openFolder(selected)
setTabFolder(ownTab.id, detail.id, detail.path)
toast.success(t("toasts.folderChanged", { name: detail.name }))
}
} catch (err) {
console.error("[ConversationContextBar] open folder failed:", err)
toast.error(t("toasts.openFolderFailed"))
}
}}
labelOpenNew={t("openNewFolder")}
labelEmpty={t("noFolders")} labelEmpty={t("noFolders")}
labelSearch={t("searchFolder")} labelSearch={t("searchFolder")}
/> />
@@ -157,21 +119,6 @@ export const ConversationContextBar = memo(function ConversationContextBar({
toast.error(msg) toast.error(msg)
} }
}} }}
onNewBranch={async (branchName, startPoint) => {
const taskId = `new-branch-${ownFolder.id}-${Date.now()}`
addTask(taskId, tBd("tasks.newBranch", { name: branchName }))
updateTask(taskId, { status: "running" })
try {
await gitNewBranch(ownFolder.path, branchName, startPoint)
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)
}
}}
/> />
</div> </div>
</TooltipProvider> </TooltipProvider>
@@ -190,8 +137,6 @@ interface FolderPickerProps {
currentFolderName: string currentFolderName: string
editable: boolean editable: boolean
onSelect: (folderId: number) => void | Promise<void> onSelect: (folderId: number) => void | Promise<void>
onOpenNewFolder: () => void | Promise<void>
labelOpenNew: string
labelEmpty: string labelEmpty: string
labelSearch: string labelSearch: string
} }
@@ -202,8 +147,6 @@ const FolderPicker = memo(function FolderPicker({
currentFolderName, currentFolderName,
editable, editable,
onSelect, onSelect,
onOpenNewFolder,
labelOpenNew,
labelEmpty, labelEmpty,
labelSearch, labelSearch,
}: FolderPickerProps) { }: FolderPickerProps) {
@@ -236,8 +179,8 @@ const FolderPicker = memo(function FolderPicker({
return ( return (
<Popover open={open} onOpenChange={setOpen}> <Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>{trigger}</PopoverTrigger> <PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent align="start" className="p-0 w-72"> <PopoverContent align="start" className="p-0 w-72 overflow-hidden">
<Command> <Command className="rounded-2xl">
<CommandInput placeholder={labelSearch} /> <CommandInput placeholder={labelSearch} />
<CommandList> <CommandList>
<CommandEmpty>{labelEmpty}</CommandEmpty> <CommandEmpty>{labelEmpty}</CommandEmpty>
@@ -264,18 +207,6 @@ const FolderPicker = memo(function FolderPicker({
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
<CommandSeparator />
<CommandGroup>
<CommandItem
onSelect={() => {
setOpen(false)
void onOpenNewFolder()
}}
>
<FolderOpen className="h-4 w-4" />
{labelOpenNew}
</CommandItem>
</CommandGroup>
</CommandList> </CommandList>
</Command> </Command>
</PopoverContent> </PopoverContent>
@@ -292,7 +223,6 @@ interface BranchPickerProps {
folderPath: string folderPath: string
currentBranch: string | null currentBranch: string | null
onCheckout: (branchName: string) => Promise<void> onCheckout: (branchName: string) => Promise<void>
onNewBranch: (branchName: string, startPoint?: string) => Promise<void>
} }
const BranchPicker = memo(function BranchPicker({ const BranchPicker = memo(function BranchPicker({
@@ -300,15 +230,12 @@ const BranchPicker = memo(function BranchPicker({
folderPath, folderPath,
currentBranch, currentBranch,
onCheckout, onCheckout,
onNewBranch,
}: BranchPickerProps) { }: BranchPickerProps) {
const t = useTranslations("Folder.conversationContextBar") const t = useTranslations("Folder.conversationContextBar")
const tBd = useTranslations("Folder.branchDropdown") const tBd = useTranslations("Folder.branchDropdown")
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [branchList, setBranchList] = useState<GitBranchList | null>(null) const [branchList, setBranchList] = useState<GitBranchList | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [newBranchOpen, setNewBranchOpen] = useState(false)
const [newBranchName, setNewBranchName] = useState("")
const loadBranches = useCallback(async () => { const loadBranches = useCallback(async () => {
setLoading(true) setLoading(true)
@@ -333,135 +260,79 @@ const BranchPicker = memo(function BranchPicker({
}, [folderId]) }, [folderId])
return ( return (
<> <Popover open={open} onOpenChange={setOpen}>
<Popover open={open} onOpenChange={setOpen}> <PopoverTrigger asChild>
<PopoverTrigger asChild> <Button variant="outline" size="xs" className="min-w-0 bg-transparent">
<Button <GitBranch className="size-3 shrink-0 text-muted-foreground" />
variant="outline" <span className="max-w-[160px] truncate">
size="xs" {currentBranch ?? t("noBranch")}
className="min-w-0 bg-transparent" </span>
> <ChevronsUpDown className="size-3 shrink-0 text-muted-foreground" />
<GitBranch className="size-3 shrink-0 text-muted-foreground" /> </Button>
<span className="max-w-[160px] truncate"> </PopoverTrigger>
{currentBranch ?? t("noBranch")} <PopoverContent align="start" className="p-0 w-80 overflow-hidden">
</span> <Command className="rounded-2xl">
<ChevronsUpDown className="size-3 shrink-0 text-muted-foreground" /> <CommandInput placeholder={t("searchBranch")} />
</Button> <CommandList>
</PopoverTrigger> {loading ? (
<PopoverContent align="start" className="p-0 w-80 overflow-hidden"> <div className="py-6 text-center text-xs text-muted-foreground">
<Command className="rounded-2xl"> <Loader2 className="h-3.5 w-3.5 animate-spin mx-auto" />
<CommandInput placeholder={t("searchBranch")} /> </div>
<CommandList> ) : (
{loading ? ( <>
<div className="py-6 text-center text-xs text-muted-foreground"> <CommandEmpty>{t("noBranches")}</CommandEmpty>
<Loader2 className="h-3.5 w-3.5 animate-spin mx-auto" /> {branchList && branchList.local.length > 0 && (
</div> <CommandGroup
) : ( heading={tBd("localBranches", {
<> count: branchList.local.length,
<CommandEmpty>{t("noBranches")}</CommandEmpty> })}
<CommandGroup> >
<CommandItem {branchList.local.map((b) => (
onSelect={() => { <CommandItem
setOpen(false) key={`local-${b}`}
setNewBranchName("") value={`local ${b}`}
setNewBranchOpen(true) onSelect={() => {
}} setOpen(false)
> if (b !== currentBranch) void onCheckout(b)
<Plus className="h-4 w-4" /> }}
{tBd("newBranch")}
</CommandItem>
</CommandGroup>
{branchList && branchList.local.length > 0 && (
<>
<CommandSeparator />
<CommandGroup
heading={tBd("localBranches", {
count: branchList.local.length,
})}
> >
{branchList.local.map((b) => ( <GitBranch className="h-4 w-4" />
<CommandItem <span className="flex-1 truncate">{b}</span>
key={`local-${b}`} {b === currentBranch && (
value={`local ${b}`} <Check className="h-4 w-4 shrink-0" />
onSelect={() => { )}
setOpen(false) </CommandItem>
if (b !== currentBranch) void onCheckout(b) ))}
}} </CommandGroup>
> )}
<GitBranch className="h-4 w-4" /> {branchList && branchList.remote.length > 0 && (
<span className="flex-1 truncate">{b}</span> <CommandGroup
{b === currentBranch && ( heading={tBd("remoteBranches", {
<Check className="h-4 w-4 shrink-0" /> count: branchList.remote.length,
)} })}
</CommandItem> >
))} {branchList.remote.map((b) => (
</CommandGroup> <CommandItem
</> key={`remote-${b}`}
)} value={`remote ${b}`}
{branchList && branchList.remote.length > 0 && ( onSelect={() => {
<CommandGroup setOpen(false)
heading={tBd("remoteBranches", { void onCheckout(b)
count: branchList.remote.length, }}
})} >
> <GitBranch className="h-4 w-4 opacity-60" />
{branchList.remote.map((b) => ( <span className="flex-1 truncate text-muted-foreground">
<CommandItem {b}
key={`remote-${b}`} </span>
value={`remote ${b}`} </CommandItem>
onSelect={() => { ))}
setOpen(false) </CommandGroup>
void onCheckout(b) )}
}} </>
> )}
<GitBranch className="h-4 w-4 opacity-60" /> </CommandList>
<span className="flex-1 truncate text-muted-foreground"> </Command>
{b} </PopoverContent>
</span> </Popover>
</CommandItem>
))}
</CommandGroup>
)}
</>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<Dialog open={newBranchOpen} onOpenChange={setNewBranchOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{tBd("dialogs.newBranchTitle")}</DialogTitle>
</DialogHeader>
<div className="text-sm text-muted-foreground">
{tBd("dialogs.newBranchDescription", {
branch: currentBranch ?? "-",
})}
</div>
<Input
placeholder={tBd("dialogs.branchNamePlaceholder")}
value={newBranchName}
onChange={(e) => setNewBranchName(e.target.value)}
autoFocus
/>
<DialogFooter>
<Button variant="outline" onClick={() => setNewBranchOpen(false)}>
{t("cancel")}
</Button>
<Button
disabled={!newBranchName.trim()}
onClick={async () => {
const name = newBranchName.trim()
if (!name) return
setNewBranchOpen(false)
await onNewBranch(name, currentBranch ?? undefined)
}}
>
{t("create")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
) )
}) })

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "暂无文件夹", "noFolders": "暂无文件夹",
"noBranches": "暂无分支", "noBranches": "暂无分支",
"noBranch": "(无分支)", "noBranch": "(无分支)",
"openNewFolder": "从磁盘打开文件夹...",
"cancel": "取消",
"create": "创建",
"commit": "提交", "commit": "提交",
"push": "推送", "push": "推送",
"merge": "合并", "merge": "合并",

View File

@@ -1797,9 +1797,6 @@
"noFolders": "No folders", "noFolders": "No folders",
"noBranches": "No branches", "noBranches": "No branches",
"noBranch": "(no branch)", "noBranch": "(no branch)",
"openNewFolder": "Open folder from disk...",
"cancel": "Cancel",
"create": "Create",
"commit": "Commit", "commit": "Commit",
"push": "Push", "push": "Push",
"merge": "Merge", "merge": "Merge",