Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 84 additions & 1 deletion src/components/layout/ChatListPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export function ChatListPanel({ open, hasUpdate, readyToInstall }: ChatListPanel
buddy?: { emoji: string; buddyName?: string; species?: string };
} | null>(null);
const [promoDismissed, setPromoDismissed] = useState(false);
// Drag a folder onto the sidebar to create a project (Electron desktop only).
// dragDepthRef is a counter that absorbs dragenter/dragleave bubbling across
// nested children, so the drop highlight doesn't flicker when the cursor
// crosses sub-elements inside the panel.
const dragDepthRef = useRef(0);
const [isDragOver, setIsDragOver] = useState(false);

// Reload assistant summary when sessions change (e.g. after onboarding/rename)
useEffect(() => {
Expand Down Expand Up @@ -126,6 +132,38 @@ export function ChatListPanel({ open, hasUpdate, readyToInstall }: ChatListPanel
}
}, [isElectron, openNativePicker, t, handleFolderSelect]);

const handleDrop = useCallback(async (e: React.DragEvent) => {
e.preventDefault();
dragDepthRef.current = 0;
setIsDragOver(false);
// Only the Electron desktop build can resolve a dropped File to a real
// filesystem path — under contextIsolation the renderer's File.path is
// intentionally zeroed, so we route through preload's webUtils helper.
const getPathForFile = window.electronAPI?.fs?.getPathForFile;
const files = e.dataTransfer?.files;
if (!getPathForFile || !files || files.length === 0) return;
const path = getPathForFile(files[0]);
if (!path) return;
// Reject non-directories up-front (the same /api/files/browse check
// handleNewChat uses) so dragging a single file produces a clear warning
// instead of a silent no-op inside handleFolderSelect.
try {
const checkRes = await fetch(`/api/files/browse?dir=${encodeURIComponent(path)}`);
if (!checkRes.ok) {
showToast({ type: 'warning', message: t('chatList.dropFileNotFolder') });
return;
}
} catch {
// Validation request itself failed — fall through and let
// handleFolderSelect's own failure path deal with it.
}
try {
await handleFolderSelect(path);
} catch {
showToast({ type: 'error', message: t('chatList.dropCreateFailed') });
}
}, [handleFolderSelect, t, showToast]);

const handleNewChat = useCallback(async () => {
let lastDir = workingDirectory
|| (typeof window !== 'undefined' ? localStorage.getItem("codepilot:last-working-directory") : null);
Expand Down Expand Up @@ -436,7 +474,33 @@ export function ChatListPanel({ open, hasUpdate, readyToInstall }: ChatListPanel
// <CardSurface kind="sidebar"> in AppShell. This inner block now
// only owns the column layout for its own children.
return (
<div className="flex h-full w-full flex-col">
<div
className="relative flex h-full w-full flex-col"
onDragEnter={(e) => {
// Electron-only + file/folder drags only. Skip in-browser runs
// (no path resolution available) and in-app text/link drags.
if (!window.electronAPI?.fs?.getPathForFile) return;
if (!e.dataTransfer?.types?.includes('Files')) return;
e.preventDefault();
dragDepthRef.current += 1;
setIsDragOver(true);
}}
onDragOver={(e) => {
// preventDefault is mandatory here — otherwise the browser would
// open the dropped file and onDrop would never fire.
if (!window.electronAPI?.fs?.getPathForFile) return;
if (!e.dataTransfer?.types?.includes('Files')) return;
e.preventDefault();
}}
onDragLeave={() => {
dragDepthRef.current -= 1;
if (dragDepthRef.current <= 0) {
dragDepthRef.current = 0;
setIsDragOver(false);
}
}}
onDrop={handleDrop}
>
{/* Round 20 — the h-12 traffic-light-safe-area + collapse
button used to live at the top of this panel. Both moved
to UnifiedTopBar so the four floating cards (this sidebar,
Expand Down Expand Up @@ -841,6 +905,25 @@ export function ChatListPanel({ open, hasUpdate, readyToInstall }: ChatListPanel
onSelect={handleFolderSelect}
/>

{/* Drag-to-create overlay — visible only while a folder is dragged
over the panel (Electron desktop only). */}
<AnimatePresence>
{isDragOver && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.12 }}
className="pointer-events-none absolute inset-0 z-50 flex flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-primary/70 bg-primary/10 backdrop-blur-[2px]"
>
<CodePilotIcon name="folder_add" size="md" className="text-primary" aria-hidden />
<span className="text-[13px] font-medium text-primary">
{t('chatList.dropToCreateProject')}
</span>
</motion.div>
)}
</AnimatePresence>

</div>
);
}
3 changes: 3 additions & 0 deletions src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ const en = {
'chatList.projects': 'Projects',
'chatList.assistantSection': 'Assistant',
'chatList.newProject': 'New Project',
'chatList.dropToCreateProject': 'Drop folder to create project',
'chatList.dropFileNotFolder': 'Please drop a folder, not a file',
'chatList.dropCreateFailed': 'Failed to create project',
'chatList.showMore': 'Show {count} more',
'chatList.showLess': 'Show less',

Expand Down
3 changes: 3 additions & 0 deletions src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const zh: Record<TranslationKey, string> = {
'chatList.projects': '项目',
'chatList.assistantSection': '助理',
'chatList.newProject': '新建项目',
'chatList.dropToCreateProject': '松开即可新建项目',
'chatList.dropFileNotFolder': '请拖入文件夹,不支持单个文件',
'chatList.dropCreateFailed': '创建项目失败',
'chatList.showMore': '展开更多({count} 条)',
'chatList.showLess': '收起',

Expand Down
Loading