Initial commit

This commit is contained in:
xggz
2026-03-06 22:56:13 +08:00
commit 54d1097b41
273 changed files with 92457 additions and 0 deletions

View File

@@ -0,0 +1,309 @@
"use client"
import {
useCallback,
useEffect,
useRef,
useState,
type KeyboardEvent as ReactKeyboardEvent,
} from "react"
import { open } from "@tauri-apps/plugin-dialog"
import {
Columns2,
FileCode2,
MessageSquare,
PanelBottom,
PanelLeft,
PanelRight,
Search,
Settings,
} from "lucide-react"
import { getGitBranch, openFolderWindow, openSettingsWindow } from "@/lib/tauri"
import { useFolderContext } from "@/contexts/folder-context"
import { Button } from "@/components/ui/button"
import { useSidebarContext } from "@/contexts/sidebar-context"
import { useAuxPanelContext } from "@/contexts/aux-panel-context"
import { useTerminalContext } from "@/contexts/terminal-context"
import { useTabContext } from "@/contexts/tab-context"
import { useWorkspaceContext } from "@/contexts/workspace-context"
import { useIsMac } from "@/hooks/use-is-mac"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import {
formatShortcutLabel,
matchShortcutEvent,
} from "@/lib/keyboard-shortcuts"
import { AppTitleBar } from "./app-title-bar"
import { FolderNameDropdown } from "./folder-name-dropdown"
import { BranchDropdown } from "./branch-dropdown"
import { CommandDropdown } from "./command-dropdown"
import { SearchCommandDialog } from "@/components/conversations/search-command-dialog"
const MODE_TABS = [
{
mode: "conversation",
title: "会话模式",
icon: MessageSquare,
},
{
mode: "fusion",
title: "融合模式",
icon: Columns2,
},
{
mode: "files",
title: "文件模式",
icon: FileCode2,
},
] as const
export function FolderTitleBar() {
const { folder } = useFolderContext()
const { isOpen, toggle } = useSidebarContext()
const { isOpen: auxPanelOpen, toggle: toggleAuxPanel } = useAuxPanelContext()
const { isOpen: terminalOpen, toggle: toggleTerminal } = useTerminalContext()
const { openNewConversationTab } = useTabContext()
const { mode, setMode } = useWorkspaceContext()
const isMac = useIsMac()
const { shortcuts } = useShortcutSettings()
const [branch, setBranch] = useState<string | null>(null)
const [searchOpen, setSearchOpen] = useState(false)
const intervalRef = useRef<ReturnType<typeof setInterval> | undefined>(
undefined
)
const folderPath = folder?.path ?? ""
const handleOpenFolder = useCallback(async () => {
try {
const selected = await open({ directory: true, multiple: false })
if (!selected) return
await openFolderWindow(selected)
} catch (err) {
console.error("[FolderTitleBar] failed to open folder:", err)
}
}, [])
const handleOpenSettings = useCallback(() => {
openSettingsWindow().catch((err) => {
console.error("[FolderTitleBar] failed to open settings:", err)
})
}, [])
useEffect(() => {
if (!folderPath) return
let cancelled = false
async function doFetch() {
if (document.visibilityState !== "visible") return
try {
const b = await getGitBranch(folderPath)
if (!cancelled) setBranch(b)
} catch {
if (!cancelled) setBranch(null)
}
}
function handleVisibilityChange() {
if (document.visibilityState === "visible") {
void doFetch()
}
}
void doFetch()
intervalRef.current = setInterval(() => {
void doFetch()
}, 10_000)
document.addEventListener("visibilitychange", handleVisibilityChange)
return () => {
cancelled = true
clearInterval(intervalRef.current)
document.removeEventListener("visibilitychange", handleVisibilityChange)
}
}, [folderPath])
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (matchShortcutEvent(e, shortcuts.toggle_search)) {
e.preventDefault()
setSearchOpen((prev) => !prev)
return
}
if (matchShortcutEvent(e, shortcuts.toggle_sidebar)) {
e.preventDefault()
toggle()
return
}
if (matchShortcutEvent(e, shortcuts.toggle_terminal)) {
e.preventDefault()
toggleTerminal()
return
}
if (matchShortcutEvent(e, shortcuts.toggle_aux_panel)) {
e.preventDefault()
toggleAuxPanel()
return
}
if (matchShortcutEvent(e, shortcuts.new_conversation)) {
if (!folderPath) return
e.preventDefault()
openNewConversationTab("codex", folderPath)
return
}
if (matchShortcutEvent(e, shortcuts.open_folder)) {
e.preventDefault()
void handleOpenFolder()
return
}
if (matchShortcutEvent(e, shortcuts.open_settings)) {
e.preventDefault()
handleOpenSettings()
}
}
document.addEventListener("keydown", handleKeyDown)
return () => document.removeEventListener("keydown", handleKeyDown)
}, [
folderPath,
handleOpenFolder,
handleOpenSettings,
openNewConversationTab,
shortcuts,
toggle,
toggleAuxPanel,
toggleTerminal,
])
const refreshBranch = useCallback(async () => {
if (!folderPath) return
try {
setBranch(await getGitBranch(folderPath))
} catch {
setBranch(null)
}
}, [folderPath])
const modeIndex = MODE_TABS.findIndex((item) => item.mode === mode)
const indicatorLeft = `${2 + modeIndex * 32}px`
const handleModeKeyDown = useCallback(
(event: ReactKeyboardEvent<HTMLDivElement>, nextMode: typeof mode) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault()
setMode(nextMode)
}
},
[setMode]
)
return (
<>
<AppTitleBar
centerInteractive
left={
<div className="flex items-center gap-4 min-w-0 pl-4">
<FolderNameDropdown />
<BranchDropdown
branch={branch}
parentBranch={folder?.parent_branch ?? null}
onBranchChange={refreshBranch}
/>
<div data-tauri-drag-region className="h-8 flex-1" />
</div>
}
center={
<div
role="tablist"
aria-label="工作区模式"
className="relative flex h-[27px] items-center rounded-full border border-border/80 bg-background/80 p-0.5"
>
<div
className="pointer-events-none absolute bottom-[2px] top-[2px] w-8 rounded-full bg-accent transition-[left] duration-300 ease-out"
style={{ left: indicatorLeft }}
/>
{MODE_TABS.map((item) => {
const Icon = item.icon
const isActive = mode === item.mode
return (
<div
key={item.mode}
role="tab"
tabIndex={0}
className={`relative z-10 m-0 flex h-[23px] w-8 cursor-pointer select-none items-center justify-center rounded-full border-0 bg-transparent p-0 align-middle leading-none transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/60 ${
isActive
? "text-foreground"
: "text-muted-foreground hover:text-foreground/80"
}`}
onClick={() => setMode(item.mode)}
onKeyDown={(event) => handleModeKeyDown(event, item.mode)}
onMouseDown={(event) => event.preventDefault()}
title={item.title}
aria-label={item.title}
aria-selected={isActive}
>
<Icon
className="block h-3 w-3 shrink-0"
shapeRendering="geometricPrecision"
/>
</div>
)
})}
</div>
}
right={
<div className="flex items-center gap-10">
<div className="flex items-center gap-2">
<CommandDropdown />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={toggle}
title={`${isOpen ? "Hide Sidebar" : "Show Sidebar"} (${formatShortcutLabel(shortcuts.toggle_sidebar, isMac)})`}
>
<PanelLeft className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${terminalOpen ? "bg-accent" : ""}`}
onClick={() => toggleTerminal()}
title={`Toggle Terminal (${formatShortcutLabel(shortcuts.toggle_terminal, isMac)})`}
>
<PanelBottom className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`}
onClick={toggleAuxPanel}
title={`Toggle Auxiliary Panel (${formatShortcutLabel(shortcuts.toggle_aux_panel, isMac)})`}
>
<PanelRight className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={() => setSearchOpen(true)}
title={`Search (${formatShortcutLabel(shortcuts.toggle_search, isMac)})`}
>
<Search className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 hover:text-foreground/80"
onClick={handleOpenSettings}
title={`Open Settings (${formatShortcutLabel(shortcuts.open_settings, isMac)})`}
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
</div>
}
/>
<SearchCommandDialog open={searchOpen} onOpenChange={setSearchOpen} />
</>
)
}