diff --git a/src/components/conversations/sidebar-conversation-list.tsx b/src/components/conversations/sidebar-conversation-list.tsx index 64986b9..b907eed 100644 --- a/src/components/conversations/sidebar-conversation-list.tsx +++ b/src/components/conversations/sidebar-conversation-list.tsx @@ -16,9 +16,12 @@ import { Virtualizer, type VirtualizerHandle } from "virtua" import { ChevronRight, Download, + FolderOpen, + GitBranch, ListChecks, Loader2, Plus, + Rocket, XCircle, } from "lucide-react" import { useActiveFolder } from "@/contexts/active-folder-context" @@ -28,10 +31,12 @@ import { useTaskContext } from "@/contexts/task-context" import { useZoomLevel } from "@/hooks/use-appearance" import { importLocalConversations, + openProjectBootWindow, updateConversationTitle, updateConversationStatus, deleteConversation, } from "@/lib/api" +import { isDesktop, openFileDialog } from "@/lib/platform" import type { ConversationStatus, DbConversationSummary } from "@/lib/types" import { loadFolderExpanded, @@ -39,6 +44,8 @@ import { } from "@/lib/sidebar-view-mode-storage" import { SidebarConversationCard } from "./sidebar-conversation-card" import { ConversationManageDialog } from "./conversation-manage-dialog" +import { CloneDialog } from "@/components/layout/clone-dialog" +import { DirectoryBrowserDialog } from "@/components/shared/directory-browser-dialog" import { Button } from "@/components/ui/button" import { Skeleton } from "@/components/ui/skeleton" import { ScrollArea } from "@/components/ui/scroll-area" @@ -261,6 +268,7 @@ export function SidebarConversationList({ }) { const t = useTranslations("Folder.sidebar") const tCommon = useTranslations("Folder.common") + const tFolderDropdown = useTranslations("Folder.folderNameDropdown") const { zoomLevel } = useZoomLevel() const safeZoomLevel = typeof zoomLevel === "number" && Number.isFinite(zoomLevel) && zoomLevel > 0 @@ -271,6 +279,7 @@ export function SidebarConversationList({ Math.round((CARD_HEIGHT_REM * 16 * safeZoomLevel) / 100) ) const { + folders, allFolders, conversations, conversationsLoading: loading, @@ -278,6 +287,7 @@ export function SidebarConversationList({ refreshConversations, updateConversationLocal, removeFolderFromWorkspace, + openFolder, } = useAppWorkspace() const refreshing = loading const { activeFolder } = useActiveFolder() @@ -320,6 +330,8 @@ export function SidebarConversationList({ folderId: number folderName: string } | null>(null) + const [cloneOpen, setCloneOpen] = useState(false) + const [browserOpen, setBrowserOpen] = useState(false) useEffect(() => { // Hydrate from localStorage after mount to keep SSR/CSR markup consistent. @@ -644,6 +656,45 @@ export function SidebarConversationList({ await handleImportForFolder(activeFolder.id) }, [activeFolder, handleImportForFolder]) + const handleOpenFolderAction = useCallback(async () => { + if (isDesktop()) { + try { + const result = await openFileDialog({ + directory: true, + multiple: false, + }) + if (!result) return + const selected = Array.isArray(result) ? result[0] : result + await openFolder(selected) + } catch (err) { + console.error("[SidebarConversationList] failed to open folder:", err) + } + } else { + setBrowserOpen(true) + } + }, [openFolder]) + + const handleBrowserSelect = useCallback( + (path: string) => { + openFolder(path).catch((err) => { + console.error("[SidebarConversationList] failed to open folder:", err) + }) + }, + [openFolder] + ) + + const handleProjectBoot = useCallback(() => { + openProjectBootWindow().catch((err) => { + console.error( + "[SidebarConversationList] failed to open project boot:", + err + ) + }) + }, []) + + const showEmptyWorkspaceActions = + folders.length === 0 && conversations.length === 0 + const emptyAfterFilter = filteredConversations.length === 0 && conversations.length > 0 @@ -667,6 +718,36 @@ export function SidebarConversationList({ {t("error", { message: error })}

+ ) : showEmptyWorkspaceActions ? ( +
+ + + +
) : conversations.length === 0 ? ( @@ -833,6 +914,13 @@ export function SidebarConversationList({ folderName={manageState.folderName} /> )} + + + ) } diff --git a/src/components/layout/aux-panel-file-tree-tab.tsx b/src/components/layout/aux-panel-file-tree-tab.tsx index 1298f66..3606af3 100644 --- a/src/components/layout/aux-panel-file-tree-tab.tsx +++ b/src/components/layout/aux-panel-file-tree-tab.tsx @@ -19,6 +19,7 @@ import { useTabContext } from "@/contexts/tab-context" import { useTerminalContext } from "@/contexts/terminal-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { useWorkspaceStateStore } from "@/hooks/use-workspace-state-store" +import { AuxPanelNoFolderEmpty } from "@/components/layout/aux-panel-no-folder-empty" import { WorkspaceDegradedBanner } from "@/components/layout/workspace-degraded-banner" import { createFileTreeEntry, @@ -2098,6 +2099,10 @@ export function FileTreeTab() { workspaceState.seq, ]) + if (!folder) { + return + } + if (loading && nodes.length === 0) { return (
diff --git a/src/components/layout/aux-panel-git-changes-tab.tsx b/src/components/layout/aux-panel-git-changes-tab.tsx index 7030310..9c00f60 100644 --- a/src/components/layout/aux-panel-git-changes-tab.tsx +++ b/src/components/layout/aux-panel-git-changes-tab.tsx @@ -38,6 +38,7 @@ import { useActiveFolder } from "@/contexts/active-folder-context" import { useTabContext } from "@/contexts/tab-context" import { useWorkspaceContext } from "@/contexts/workspace-context" import { useWorkspaceStateStore } from "@/hooks/use-workspace-state-store" +import { AuxPanelNoFolderEmpty } from "@/components/layout/aux-panel-no-folder-empty" import { WorkspaceDegradedBanner } from "@/components/layout/workspace-degraded-banner" import { deleteFileTreeEntry, @@ -1166,6 +1167,10 @@ export function GitChangesTab() { ] ) + if (!folder) { + return + } + if (loading) { return (
diff --git a/src/components/layout/aux-panel-git-log-tab.tsx b/src/components/layout/aux-panel-git-log-tab.tsx index 079ba45..ffdb270 100644 --- a/src/components/layout/aux-panel-git-log-tab.tsx +++ b/src/components/layout/aux-panel-git-log-tab.tsx @@ -76,6 +76,7 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible" import { Skeleton } from "@/components/ui/skeleton" +import { AuxPanelNoFolderEmpty } from "@/components/layout/aux-panel-no-folder-empty" import { subscribe } from "@/lib/platform" import { useActiveFolder } from "@/contexts/active-folder-context" import { useWorkspaceContext } from "@/contexts/workspace-context" @@ -1024,6 +1025,10 @@ export function GitLogTab() { setScrolled((prev) => (prev === nextScrolled ? prev : nextScrolled)) }, []) + if (!folder) { + return + } + if (loading) { return ( diff --git a/src/components/layout/aux-panel-no-folder-empty.tsx b/src/components/layout/aux-panel-no-folder-empty.tsx new file mode 100644 index 0000000..a23c789 --- /dev/null +++ b/src/components/layout/aux-panel-no-folder-empty.tsx @@ -0,0 +1,15 @@ +"use client" + +import { FolderOpen } from "lucide-react" +import { useTranslations } from "next-intl" + +export function AuxPanelNoFolderEmpty() { + const t = useTranslations("Folder.auxPanel") + return ( +
+ +

{t("noFolderTitle")}

+

{t("noFolderHint")}

+
+ ) +} diff --git a/src/components/layout/folder-title-bar.tsx b/src/components/layout/folder-title-bar.tsx index 7384fe8..98fcbb5 100644 --- a/src/components/layout/folder-title-bar.tsx +++ b/src/components/layout/folder-title-bar.tsx @@ -344,11 +344,17 @@ export function FolderTitleBar() { - + {tTitleBar("toggleAuxPanel")} - toggleTerminal()}> + toggleTerminal()} + disabled={!activeFolder} + > {tTitleBar("toggleTerminal")} @@ -370,6 +376,7 @@ export function FolderTitleBar() { size="icon" className={`h-6 w-6 hover:text-foreground/80 ${terminalOpen ? "bg-accent" : ""}`} onClick={() => toggleTerminal()} + disabled={!activeFolder} title={tTitleBar("withShortcut", { label: tTitleBar("toggleTerminal"), shortcut: formatShortcutLabel( @@ -385,6 +392,7 @@ export function FolderTitleBar() { size="icon" className={`h-6 w-6 hover:text-foreground/80 ${auxPanelOpen ? "bg-accent" : ""}`} onClick={toggleAuxPanel} + disabled={!activeFolder} title={tTitleBar("withShortcut", { label: tTitleBar("toggleAuxPanel"), shortcut: formatShortcutLabel( diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index a841e21..0140197 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -935,7 +935,9 @@ "files": "الملفات", "changes": "التغييرات", "commits": "الالتزامات" - } + }, + "noFolderTitle": "لا يوجد مجلد مفتوح", + "noFolderHint": "افتح مجلدا لعرض محتواه هنا" }, "windowControls": { "minimizeWindow": "تصغير النافذة", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index a257dd9..1d34cf0 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -935,7 +935,9 @@ "files": "Dateien", "changes": "Änderungen", "commits": "Einträge" - } + }, + "noFolderTitle": "Kein Ordner geöffnet", + "noFolderHint": "Öffnen Sie einen Ordner, um seinen Inhalt hier zu sehen" }, "windowControls": { "minimizeWindow": "Fenster minimieren", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 412652e..03feaea 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -935,7 +935,9 @@ "files": "Files", "changes": "Changes", "commits": "Commits" - } + }, + "noFolderTitle": "No folder open", + "noFolderHint": "Open a folder to see its contents here" }, "windowControls": { "minimizeWindow": "Minimize window", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index fb9b046..5b68689 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -935,7 +935,9 @@ "files": "Archivos", "changes": "Cambios", "commits": "Confirmaciones" - } + }, + "noFolderTitle": "No hay carpeta abierta", + "noFolderHint": "Abre una carpeta para ver su contenido aquí" }, "windowControls": { "minimizeWindow": "Minimizar ventana", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 52a801f..27a3156 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -935,7 +935,9 @@ "files": "Fichiers", "changes": "Changements", "commits": "Validations" - } + }, + "noFolderTitle": "Aucun dossier ouvert", + "noFolderHint": "Ouvrez un dossier pour voir son contenu ici" }, "windowControls": { "minimizeWindow": "Minimiser la fenêtre", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 108d6a2..ba04950 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -935,7 +935,9 @@ "files": "ファイル", "changes": "変更", "commits": "コミット" - } + }, + "noFolderTitle": "開いているフォルダがありません", + "noFolderHint": "フォルダを開くとここに表示されます" }, "windowControls": { "minimizeWindow": "ウィンドウを最小化", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index b461305..4c07a3e 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -935,7 +935,9 @@ "files": "파일", "changes": "변경사항", "commits": "커밋" - } + }, + "noFolderTitle": "열린 폴더 없음", + "noFolderHint": "폴더를 열면 여기에 표시됩니다" }, "windowControls": { "minimizeWindow": "창 최소화", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 03479d9..e8cf838 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -935,7 +935,9 @@ "files": "Arquivos", "changes": "Alterações", "commits": "Confirmações" - } + }, + "noFolderTitle": "Nenhuma pasta aberta", + "noFolderHint": "Abra uma pasta para ver seu conteúdo aqui" }, "windowControls": { "minimizeWindow": "Minimizar janela", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 5a79ae4..b84c668 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -935,7 +935,9 @@ "files": "文件", "changes": "变更", "commits": "提交" - } + }, + "noFolderTitle": "暂无打开的文件夹", + "noFolderHint": "打开一个文件夹后在这里查看" }, "windowControls": { "minimizeWindow": "最小化窗口", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 7008b32..117f862 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -935,7 +935,9 @@ "files": "檔案", "changes": "變更", "commits": "提交" - } + }, + "noFolderTitle": "尚無開啟的資料夾", + "noFolderHint": "開啟一個資料夾後可在此查看" }, "windowControls": { "minimizeWindow": "最小化視窗",