From c691fb0c078bc4d674b73af54c4304ba1d72dae6 Mon Sep 17 00:00:00 2001
From: xintaofei
Date: Wed, 22 Apr 2026 10:36:27 +0800
Subject: [PATCH] feat(workspace): add no-folder empty states and gate
folder-only actions
- Sidebar: replace the "no conversations" placeholder with Open Folder, Clone Repository, and Project Boot buttons when the workspace has no open folders.
- Title bar: disable the terminal and auxiliary-panel toggle buttons while no folder is active.
- Aux panel: show a shared localized "no folder open" prompt in the file tree, git changes, and git log tabs when no folder is active.
- Add auxPanel.noFolderTitle / noFolderHint translations across all ten supported locales.
---
.../sidebar-conversation-list.tsx | 88 +++++++++++++++++++
.../layout/aux-panel-file-tree-tab.tsx | 5 ++
.../layout/aux-panel-git-changes-tab.tsx | 5 ++
.../layout/aux-panel-git-log-tab.tsx | 5 ++
.../layout/aux-panel-no-folder-empty.tsx | 15 ++++
src/components/layout/folder-title-bar.tsx | 12 ++-
src/i18n/messages/ar.json | 4 +-
src/i18n/messages/de.json | 4 +-
src/i18n/messages/en.json | 4 +-
src/i18n/messages/es.json | 4 +-
src/i18n/messages/fr.json | 4 +-
src/i18n/messages/ja.json | 4 +-
src/i18n/messages/ko.json | 4 +-
src/i18n/messages/pt.json | 4 +-
src/i18n/messages/zh-CN.json | 4 +-
src/i18n/messages/zh-TW.json | 4 +-
16 files changed, 158 insertions(+), 12 deletions(-)
create mode 100644 src/components/layout/aux-panel-no-folder-empty.tsx
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": "最小化視窗",