Chat input: drag-drop files and folders from file tree with @folder autocomplete (#963)
* feat(chat): drag-drop files/folders from file tree into chat input Features: - Support drag-and-drop files and folders from file tree into chat input - Support @folder autocomplete for file/folder mentions in chat input - Add drag support to file tree nodes for dropping into chat input - Persist confirmed mentions to localStorage across sessions Fixes: - Fix drag state getting stuck with drag enter counter and onDragEnd cleanup - Fix pendingSearchRef counter leak on query cancellation - Fix confirmed mentions highlight/delete for folder paths - Fix clear confirmed mentions on send and autocomplete loading state - Guard drag-start against empty and root-relative paths - Reset loading state when pendingSearchRef reaches zero * fix(chat): address Greptile PR review findings - P1: move confirmedMentionsRef.clear() out of handleQueueMessage to prevent bare-name mentions from being lost when queued messages are sent via handleSubmit - P2: distinguish internal file-tree drag from external file drag in overlay text (Drop to insert as mention vs Drop files here to attach) - P2: add directories to marquee overflow effect dependency array * fix(chat): prune stale confirmed mentions --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
36b177dc81
commit
bc6cb91954
@@ -85,6 +85,18 @@ const normalizePath = (value: string): string => {
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const getRelativePath = (root: string, path: string): string => {
|
||||
const normalizedPath = normalizePath(path);
|
||||
const normalizedRoot = normalizePath(root).replace(/\/+$/, '');
|
||||
if (normalizedPath === normalizedRoot) {
|
||||
return '.';
|
||||
}
|
||||
if (!normalizedRoot || !normalizedPath.startsWith(`${normalizedRoot}/`)) {
|
||||
return normalizedPath;
|
||||
}
|
||||
return normalizedPath.slice(normalizedRoot.length + 1);
|
||||
};
|
||||
|
||||
const isAbsolutePath = (value: string): boolean => {
|
||||
return value.startsWith('/') || value.startsWith('//') || /^[A-Za-z]:\//.test(value);
|
||||
};
|
||||
@@ -122,6 +134,7 @@ const FileStatusDot: React.FC<{ status: FileStatus }> = ({ status }) => {
|
||||
|
||||
interface FileRowProps {
|
||||
node: FileNode;
|
||||
root: string;
|
||||
isExpanded: boolean;
|
||||
isActive: boolean;
|
||||
status?: FileStatus | null;
|
||||
@@ -144,6 +157,7 @@ interface FileRowProps {
|
||||
|
||||
const FileRow: React.FC<FileRowProps> = ({
|
||||
node,
|
||||
root,
|
||||
isExpanded,
|
||||
isActive,
|
||||
status,
|
||||
@@ -179,6 +193,13 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
setContextMenuPath(node.path);
|
||||
}, [node.path, setContextMenuPath]);
|
||||
|
||||
const handleDragStart = React.useCallback((e: React.DragEvent) => {
|
||||
const path = getRelativePath(root, node.path);
|
||||
if (!path || path === '.') return;
|
||||
e.dataTransfer.setData('application/x-openchamber-file-path', path);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
}, [node.path, root]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group relative flex items-center"
|
||||
@@ -188,9 +209,12 @@ const FileRow: React.FC<FileRowProps> = ({
|
||||
type="button"
|
||||
onClick={handleInteraction}
|
||||
onContextMenu={handleContextMenu}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors pr-8 select-none',
|
||||
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
|
||||
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40',
|
||||
'cursor-grab active:cursor-grabbing'
|
||||
)}
|
||||
>
|
||||
{isDir ? (
|
||||
@@ -755,6 +779,7 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
)}
|
||||
<FileRow
|
||||
node={node}
|
||||
root={root}
|
||||
isExpanded={isExpanded}
|
||||
isActive={isActive}
|
||||
status={!isDir ? getFileStatus(node.path) : undefined}
|
||||
@@ -848,8 +873,15 @@ export const SidebarFilesTree: React.FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleOpenFile(node)}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
const path = node.relativePath || getRelativePath(root ?? '', node.path);
|
||||
if (!path || path === '.') return;
|
||||
e.dataTransfer.setData('application/x-openchamber-file-path', path);
|
||||
e.dataTransfer.effectAllowed = 'copy';
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors',
|
||||
'flex w-full items-center gap-1.5 rounded-md px-2 py-1 text-left text-foreground transition-colors cursor-grab active:cursor-grabbing',
|
||||
isActive ? 'bg-interactive-selection/70' : 'hover:bg-interactive-hover/40'
|
||||
)}
|
||||
title={node.path}
|
||||
|
||||
Reference in New Issue
Block a user