feat: add search functionality (#243)
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
|||||||
} from '@/lib/local-storage';
|
} from '@/lib/local-storage';
|
||||||
import { useToast } from '@/components/toast-provider';
|
import { useToast } from '@/components/toast-provider';
|
||||||
import { InlineFileInput } from './inline-file-input';
|
import { InlineFileInput } from './inline-file-input';
|
||||||
|
import { PanelHeaderBar, PanelHeaderBarSplit } from './panel-primitives';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge a freshly-fetched tree node into an existing one, preserving
|
* Merge a freshly-fetched tree node into an existing one, preserving
|
||||||
@@ -49,13 +50,16 @@ function mergeTreeNodes(existing: FileTreeNode, incoming: FileTreeNode): FileTre
|
|||||||
return { ...existing, ...incoming, children: mergedChildren };
|
return { ...existing, ...incoming, children: mergedChildren };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sort comparator: directories first, then alphabetical by name. */
|
||||||
|
const compareTreeNodes = (a: FileTreeNode, b: FileTreeNode): number => {
|
||||||
|
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
};
|
||||||
|
|
||||||
/** Insert a file node into a parent folder, keeping children sorted (dirs first, then alpha). */
|
/** Insert a file node into a parent folder, keeping children sorted (dirs first, then alpha). */
|
||||||
function insertNodeInTree(root: FileTreeNode, parentPath: string, node: FileTreeNode): FileTreeNode {
|
function insertNodeInTree(root: FileTreeNode, parentPath: string, node: FileTreeNode): FileTreeNode {
|
||||||
if (root.path === parentPath || (parentPath === '' && root.path === '')) {
|
if (root.path === parentPath || (parentPath === '' && root.path === '')) {
|
||||||
const children = [...(root.children ?? []), node].sort((a, b) => {
|
const children = [...(root.children ?? []), node].sort(compareTreeNodes);
|
||||||
if (a.is_dir !== b.is_dir) return a.is_dir ? -1 : 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
return { ...root, children };
|
return { ...root, children };
|
||||||
}
|
}
|
||||||
if (!root.children) return root;
|
if (!root.children) return root;
|
||||||
@@ -77,12 +81,10 @@ type FileBrowserProps = {
|
|||||||
onOpenFile: (file: OpenFileTab) => void;
|
onOpenFile: (file: OpenFileTab) => void;
|
||||||
onCreateFile?: (path: string) => Promise<boolean>;
|
onCreateFile?: (path: string) => Promise<boolean>;
|
||||||
onDeleteFile?: (path: string) => Promise<boolean>;
|
onDeleteFile?: (path: string) => Promise<boolean>;
|
||||||
isSearchOpen?: boolean;
|
|
||||||
onCloseSearch?: () => void;
|
|
||||||
activeFilePath?: string | null;
|
activeFilePath?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, isSearchOpen = false, onCloseSearch, activeFilePath }: FileBrowserProps) {
|
export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile, activeFilePath }: FileBrowserProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [tree, setTree] = useState<FileTreeNode | null>(null);
|
const [tree, setTree] = useState<FileTreeNode | null>(null);
|
||||||
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());
|
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());
|
||||||
@@ -99,6 +101,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
const { copied, copy: copyPath } = useCopyToClipboard(1000);
|
const { copied, copy: copyPath } = useCopyToClipboard(1000);
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
|
const [isSearchActive, setIsSearchActive] = useState(false);
|
||||||
const [localSearchQuery, setLocalSearchQuery] = useState('');
|
const [localSearchQuery, setLocalSearchQuery] = useState('');
|
||||||
const [searchResults, setSearchResults] = useState<string[] | null>(null);
|
const [searchResults, setSearchResults] = useState<string[] | null>(null);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
@@ -176,14 +179,14 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
|
|
||||||
// Focus search input when search opens
|
// Focus search input when search opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSearchOpen && searchInputRef.current) {
|
if (isSearchActive && searchInputRef.current) {
|
||||||
searchInputRef.current.focus();
|
searchInputRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [isSearchOpen]);
|
}, [isSearchActive]);
|
||||||
|
|
||||||
// Clear search when closing
|
// Clear search when closing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isSearchOpen) {
|
if (!isSearchActive) {
|
||||||
setLocalSearchQuery('');
|
setLocalSearchQuery('');
|
||||||
setSearchResults(null);
|
setSearchResults(null);
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
@@ -191,7 +194,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
clearTimeout(searchTimeoutRef.current);
|
clearTimeout(searchTimeoutRef.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isSearchOpen]);
|
}, [isSearchActive]);
|
||||||
|
|
||||||
const clearRetryTimer = useCallback(() => {
|
const clearRetryTimer = useCallback(() => {
|
||||||
if (retryTimerRef.current) {
|
if (retryTimerRef.current) {
|
||||||
@@ -474,16 +477,16 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
}, 300);
|
}, 300);
|
||||||
}, [sessionId]);
|
}, [sessionId]);
|
||||||
|
|
||||||
// Close search (clears and notifies parent)
|
// Close search and clear state
|
||||||
const handleCloseSearch = useCallback(() => {
|
const handleCloseSearch = useCallback(() => {
|
||||||
|
setIsSearchActive(false);
|
||||||
setLocalSearchQuery('');
|
setLocalSearchQuery('');
|
||||||
setSearchResults(null);
|
setSearchResults(null);
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
if (searchTimeoutRef.current) {
|
if (searchTimeoutRef.current) {
|
||||||
clearTimeout(searchTimeoutRef.current);
|
clearTimeout(searchTimeoutRef.current);
|
||||||
}
|
}
|
||||||
onCloseSearch?.();
|
}, []);
|
||||||
}, [onCloseSearch]);
|
|
||||||
|
|
||||||
// Cleanup search timeout on unmount
|
// Cleanup search timeout on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -547,39 +550,6 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
}
|
}
|
||||||
}, [expandedPaths, sessionId, tree, showLoading, hideLoading]);
|
}, [expandedPaths, sessionId, tree, showLoading, hideLoading]);
|
||||||
|
|
||||||
const openFile = useCallback(async (node: FileTreeNode) => {
|
|
||||||
if (node.is_dir) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const client = getWebSocketClient();
|
|
||||||
if (!client) return;
|
|
||||||
|
|
||||||
const response: FileContentResponse = await requestFileContent(client, sessionId, node.path);
|
|
||||||
|
|
||||||
// Calculate hash for the file content
|
|
||||||
const { calculateHash } = await import('@/lib/utils/file-diff');
|
|
||||||
const hash = await calculateHash(response.content);
|
|
||||||
|
|
||||||
onOpenFile({
|
|
||||||
path: node.path,
|
|
||||||
name: node.name,
|
|
||||||
content: response.content,
|
|
||||||
originalContent: response.content,
|
|
||||||
originalHash: hash,
|
|
||||||
isDirty: false,
|
|
||||||
isBinary: response.is_binary,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const reason = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
toast({
|
|
||||||
title: 'Failed to open file',
|
|
||||||
description: reason,
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [sessionId, onOpenFile, toast]);
|
|
||||||
|
|
||||||
// Open file from search result (path only)
|
|
||||||
const openFileByPath = useCallback(async (path: string) => {
|
const openFileByPath = useCallback(async (path: string) => {
|
||||||
try {
|
try {
|
||||||
const client = getWebSocketClient();
|
const client = getWebSocketClient();
|
||||||
@@ -587,11 +557,9 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
|
|
||||||
const response: FileContentResponse = await requestFileContent(client, sessionId, path);
|
const response: FileContentResponse = await requestFileContent(client, sessionId, path);
|
||||||
|
|
||||||
// Calculate hash for the file content
|
|
||||||
const { calculateHash } = await import('@/lib/utils/file-diff');
|
const { calculateHash } = await import('@/lib/utils/file-diff');
|
||||||
const hash = await calculateHash(response.content);
|
const hash = await calculateHash(response.content);
|
||||||
|
|
||||||
// Extract filename from path
|
|
||||||
const name = path.split('/').pop() || path;
|
const name = path.split('/').pop() || path;
|
||||||
|
|
||||||
onOpenFile({
|
onOpenFile({
|
||||||
@@ -629,7 +597,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
isActiveFolder && "bg-muted/50"
|
isActiveFolder && "bg-muted/50"
|
||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${depth * 12 + 8 + (node.is_dir ? 0 : 20)}px` }}
|
style={{ paddingLeft: `${depth * 12 + 8 + (node.is_dir ? 0 : 20)}px` }}
|
||||||
onClick={() => (node.is_dir ? toggleExpand(node) : openFile(node))}
|
onClick={() => (node.is_dir ? toggleExpand(node) : openFileByPath(node.path))}
|
||||||
>
|
>
|
||||||
{node.is_dir && (
|
{node.is_dir && (
|
||||||
<span className="flex-shrink-0">
|
<span className="flex-shrink-0">
|
||||||
@@ -698,12 +666,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{node.children && [...node.children]
|
{node.children && [...node.children]
|
||||||
.sort((a, b) => {
|
.sort(compareTreeNodes)
|
||||||
// Folders first, then alphabetically
|
|
||||||
if (a.is_dir && !b.is_dir) return -1;
|
|
||||||
if (!a.is_dir && b.is_dir) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
})
|
|
||||||
.map((child) => renderTreeNode(child, depth + 1))}
|
.map((child) => renderTreeNode(child, depth + 1))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -755,102 +718,117 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
{/* Search input - only shown when search is open */}
|
{/* Header toolbar */}
|
||||||
{isSearchOpen && (
|
{tree && loadState === 'loaded' && (
|
||||||
<div className="px-2 pb-2">
|
isSearchActive ? (
|
||||||
<div className="relative">
|
<PanelHeaderBar className="group/header">
|
||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<IconLoader2 className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none animate-spin" />
|
<IconLoader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin shrink-0" />
|
||||||
) : (
|
) : (
|
||||||
<IconSearch className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground pointer-events-none" />
|
<IconSearch className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
||||||
)}
|
)}
|
||||||
<Input
|
<Input
|
||||||
ref={searchInputRef}
|
ref={searchInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
value={localSearchQuery}
|
value={localSearchQuery}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Escape') handleCloseSearch(); }}
|
||||||
placeholder="Search files..."
|
placeholder="Search files..."
|
||||||
className="pl-7 pr-7 h-7 text-xs"
|
className="flex-1 min-w-0 h-5 text-xs border-none bg-transparent shadow-none focus-visible:ring-0 px-2"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
className="text-muted-foreground hover:text-foreground shrink-0 cursor-pointer"
|
||||||
onClick={handleCloseSearch}
|
onClick={handleCloseSearch}
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
>
|
||||||
<IconX className="h-3.5 w-3.5" />
|
<IconX className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</PanelHeaderBar>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<PanelHeaderBarSplit
|
||||||
|
className="group/header"
|
||||||
{/* Header toolbar */}
|
left={
|
||||||
{tree && loadState === 'loaded' && (
|
<>
|
||||||
<div className="group/header flex items-center gap-1 border-b border-border/50 px-2 py-1">
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
<button
|
||||||
<button
|
className="relative shrink-0 cursor-pointer"
|
||||||
className="relative shrink-0 cursor-pointer"
|
onClick={() => {
|
||||||
onClick={() => {
|
if (fullPath) void copyPath(fullPath);
|
||||||
if (fullPath) void copyPath(fullPath);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<IconFolderOpen className={cn("h-3.5 w-3.5 text-muted-foreground transition-opacity", copied ? "opacity-0" : "group-hover/header:opacity-0")} />
|
||||||
<IconFolderOpen className={cn("h-3.5 w-3.5 text-muted-foreground transition-opacity", copied ? "opacity-0" : "group-hover/header:opacity-0")} />
|
{copied ? (
|
||||||
{copied ? (
|
<IconCheck className="absolute inset-0 h-3.5 w-3.5 text-green-600/70" />
|
||||||
<IconCheck className="absolute inset-0 h-3.5 w-3.5 text-green-600/70" />
|
) : (
|
||||||
) : (
|
<IconCopy className="absolute inset-0 h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover/header:opacity-100 hover:text-foreground transition-opacity" />
|
||||||
<IconCopy className="absolute inset-0 h-3.5 w-3.5 text-muted-foreground opacity-0 group-hover/header:opacity-100 hover:text-foreground transition-opacity" />
|
)}
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Copy workspace path</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<span className="min-w-0 truncate text-xs font-medium text-muted-foreground">
|
||||||
|
{displayPath}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
right={
|
||||||
|
<>
|
||||||
|
{onCreateFile && (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
||||||
|
onClick={handleStartCreate}
|
||||||
|
>
|
||||||
|
<IconPlus className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>New file</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</button>
|
<Tooltip>
|
||||||
</TooltipTrigger>
|
<TooltipTrigger asChild>
|
||||||
<TooltipContent>Copy workspace path</TooltipContent>
|
<button
|
||||||
</Tooltip>
|
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
||||||
<span className="min-w-0 flex-1 truncate text-xs font-medium text-muted-foreground">
|
onClick={() => openFolder()}
|
||||||
{displayPath}
|
>
|
||||||
</span>
|
<IconFolderShare className="h-3.5 w-3.5" />
|
||||||
<div className="flex shrink-0 items-center gap-0.5">
|
</button>
|
||||||
{onCreateFile && (
|
</TooltipTrigger>
|
||||||
<Tooltip>
|
<TooltipContent>Open workspace folder</TooltipContent>
|
||||||
<TooltipTrigger asChild>
|
</Tooltip>
|
||||||
<button
|
<Tooltip>
|
||||||
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
<TooltipTrigger asChild>
|
||||||
onClick={handleStartCreate}
|
<button
|
||||||
>
|
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
||||||
<IconPlus className="h-3.5 w-3.5" />
|
onClick={() => setIsSearchActive(true)}
|
||||||
</button>
|
>
|
||||||
</TooltipTrigger>
|
<IconSearch className="h-3.5 w-3.5" />
|
||||||
<TooltipContent>New file</TooltipContent>
|
</button>
|
||||||
</Tooltip>
|
</TooltipTrigger>
|
||||||
)}
|
<TooltipContent>Search files</TooltipContent>
|
||||||
<Tooltip>
|
</Tooltip>
|
||||||
<TooltipTrigger asChild>
|
{expandedPaths.size > 0 && (
|
||||||
<button
|
<Tooltip>
|
||||||
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
<TooltipTrigger asChild>
|
||||||
onClick={() => openFolder()}
|
<button
|
||||||
>
|
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
||||||
<IconFolderShare className="h-3.5 w-3.5" />
|
onClick={collapseAll}
|
||||||
</button>
|
>
|
||||||
</TooltipTrigger>
|
<IconListTree className="h-3.5 w-3.5" />
|
||||||
<TooltipContent>Open workspace folder</TooltipContent>
|
</button>
|
||||||
</Tooltip>
|
</TooltipTrigger>
|
||||||
{expandedPaths.size > 0 && (
|
<TooltipContent>Collapse all</TooltipContent>
|
||||||
<Tooltip>
|
</Tooltip>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<button
|
</>
|
||||||
className="text-muted-foreground hover:bg-muted hover:text-foreground rounded p-1 cursor-pointer"
|
}
|
||||||
onClick={collapseAll}
|
/>
|
||||||
>
|
)
|
||||||
<IconListTree className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Collapse all</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollArea className="flex-1" ref={scrollAreaRef}>
|
<ScrollArea className="flex-1" ref={scrollAreaRef}>
|
||||||
{isSearchOpen && searchResults !== null ? (
|
{isSearchActive && searchResults !== null ? (
|
||||||
renderSearchResults()
|
renderSearchResults()
|
||||||
) : isSessionFailed ? (
|
) : isSessionFailed ? (
|
||||||
<div className="p-4 text-sm text-destructive/80 space-y-2">
|
<div className="p-4 text-sm text-destructive/80 space-y-2">
|
||||||
@@ -887,11 +865,7 @@ export function FileBrowser({ sessionId, onOpenFile, onCreateFile, onDeleteFile,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tree.children && [...tree.children]
|
{tree.children && [...tree.children]
|
||||||
.sort((a, b) => {
|
.sort(compareTreeNodes)
|
||||||
if (a.is_dir && !b.is_dir) return -1;
|
|
||||||
if (!a.is_dir && b.is_dir) return 1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
})
|
|
||||||
.map((child) => renderTreeNode(child, 0))}
|
.map((child) => renderTreeNode(child, 0))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { memo, useState, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { PanelRoot, PanelBody } from './panel-primitives';
|
import { PanelRoot, PanelBody } from './panel-primitives';
|
||||||
import { useAppStore } from '@/components/state-provider';
|
import { useAppStore } from '@/components/state-provider';
|
||||||
import { useFileOperations } from '@/hooks/use-file-operations';
|
import { useFileOperations } from '@/hooks/use-file-operations';
|
||||||
@@ -13,7 +13,6 @@ type FilesPanelProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FilesPanel = memo(function FilesPanel({ onOpenFile }: FilesPanelProps) {
|
const FilesPanel = memo(function FilesPanel({ onOpenFile }: FilesPanelProps) {
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
||||||
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
|
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
|
||||||
const isArchived = useIsTaskArchived();
|
const isArchived = useIsTaskArchived();
|
||||||
const { createFile, deleteFile } = useFileOperations(activeSessionId ?? null);
|
const { createFile, deleteFile } = useFileOperations(activeSessionId ?? null);
|
||||||
@@ -40,8 +39,6 @@ const FilesPanel = memo(function FilesPanel({ onOpenFile }: FilesPanelProps) {
|
|||||||
onOpenFile={onOpenFile}
|
onOpenFile={onOpenFile}
|
||||||
onCreateFile={handleCreateFile}
|
onCreateFile={handleCreateFile}
|
||||||
onDeleteFile={deleteFile}
|
onDeleteFile={deleteFile}
|
||||||
isSearchOpen={isSearchOpen}
|
|
||||||
onCloseSearch={() => setIsSearchOpen(false)}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full text-muted-foreground text-xs">
|
<div className="flex items-center justify-center h-full text-muted-foreground text-xs">
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconGitCommit,
|
IconGitCommit,
|
||||||
IconLoader2,
|
IconLoader2,
|
||||||
IconSearch,
|
|
||||||
IconColumns,
|
IconColumns,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
@@ -75,7 +74,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
|
|||||||
const [topTab, setTopTab] = useState<'diff' | 'files'>('diff');
|
const [topTab, setTopTab] = useState<'diff' | 'files'>('diff');
|
||||||
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
|
const [showDiscardDialog, setShowDiscardDialog] = useState(false);
|
||||||
const [fileToDiscard, setFileToDiscard] = useState<string | null>(null);
|
const [fileToDiscard, setFileToDiscard] = useState<string | null>(null);
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
|
||||||
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
|
const activeSessionId = useAppStore((state) => state.tasks.activeSessionId);
|
||||||
const gitStatus = useSessionGitStatus(activeSessionId);
|
const gitStatus = useSessionGitStatus(activeSessionId);
|
||||||
const { commits } = useSessionCommits(activeSessionId ?? null);
|
const { commits } = useSessionCommits(activeSessionId ?? null);
|
||||||
@@ -93,11 +91,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
|
|||||||
// Track previous changed files count for auto-switch detection
|
// Track previous changed files count for auto-switch detection
|
||||||
const prevChangedCountRef = useRef(0);
|
const prevChangedCountRef = useRef(0);
|
||||||
|
|
||||||
// Close search when switching tabs
|
|
||||||
useEffect(() => {
|
|
||||||
setIsSearchOpen(false);
|
|
||||||
}, [topTab]);
|
|
||||||
|
|
||||||
// State for commit diffs
|
// State for commit diffs
|
||||||
const [expandedCommit, setExpandedCommit] = useState<string | null>(null);
|
const [expandedCommit, setExpandedCommit] = useState<string | null>(null);
|
||||||
const [commitDiffs, setCommitDiffs] = useState<Record<string, Record<string, FileInfo>>>({});
|
const [commitDiffs, setCommitDiffs] = useState<Record<string, Record<string, FileInfo>>>({});
|
||||||
@@ -344,28 +337,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Contextual buttons based on active tab
|
|
||||||
const tabRightContent = useMemo(() => {
|
|
||||||
if (topTab === 'files') {
|
|
||||||
return (
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="text-muted-foreground hover:text-foreground cursor-pointer"
|
|
||||||
onClick={() => setIsSearchOpen(true)}
|
|
||||||
>
|
|
||||||
<IconSearch className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Search files</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// No contextual buttons for diff tab (for now)
|
|
||||||
return null;
|
|
||||||
}, [topTab]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SessionPanel borderSide="left">
|
<SessionPanel borderSide="left">
|
||||||
<SessionTabs
|
<SessionTabs
|
||||||
@@ -373,7 +344,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
|
|||||||
activeTab={topTab}
|
activeTab={topTab}
|
||||||
onTabChange={(value) => handleTabChange(value as 'diff' | 'files')}
|
onTabChange={(value) => handleTabChange(value as 'diff' | 'files')}
|
||||||
className="flex-1 min-h-0"
|
className="flex-1 min-h-0"
|
||||||
rightContent={tabRightContent}
|
|
||||||
>
|
>
|
||||||
<TabsContent value="diff" className="flex-1 min-h-0">
|
<TabsContent value="diff" className="flex-1 min-h-0">
|
||||||
<SessionPanelContent className="flex flex-col">
|
<SessionPanelContent className="flex flex-col">
|
||||||
@@ -604,8 +574,6 @@ const TaskFilesPanel = memo(function TaskFilesPanel({ onSelectDiff, onOpenFile,
|
|||||||
onOpenFile={onOpenFile}
|
onOpenFile={onOpenFile}
|
||||||
onCreateFile={handleCreateFile}
|
onCreateFile={handleCreateFile}
|
||||||
onDeleteFile={hookDeleteFile}
|
onDeleteFile={hookDeleteFile}
|
||||||
isSearchOpen={isSearchOpen}
|
|
||||||
onCloseSearch={() => setIsSearchOpen(false)}
|
|
||||||
activeFilePath={activeFilePath}
|
activeFilePath={activeFilePath}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user