Skip to content

Commit b9b2932

Browse files
author
catlog22
committed
Add tests and implement functionality for staged cascade search and LSP expansion
- Introduced a new JSON file for verbose output of the Codex Lens search results. - Added unit tests for binary search functionality in `test_stage1_binary_search_uses_chunk_lines.py`. - Implemented regression tests for staged cascade Stage 2 expansion depth in `test_staged_cascade_lsp_depth.py`. - Created unit tests for staged cascade Stage 2 realtime LSP graph expansion in `test_staged_cascade_realtime_lsp.py`. - Enhanced the ChainSearchEngine to respect configuration settings for staged LSP depth and improve search accuracy.
1 parent 166211d commit b9b2932

20 files changed

Lines changed: 1878 additions & 279 deletions

ccw/frontend/src/components/workspace/WorkspaceSelector.tsx

Lines changed: 21 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
// ========================================
22
// Workspace Selector Component
33
// ========================================
4-
// Dropdown for selecting recent workspaces with folder browser and manual path input
4+
// Dropdown for selecting recent workspaces with native folder picker and manual path input
55

6-
import { useState, useCallback, useRef } from 'react';
6+
import { useState, useCallback } from 'react';
77
import { ChevronDown, X, FolderOpen, Check } from 'lucide-react';
88
import { useIntl } from 'react-intl';
99
import { cn } from '@/lib/utils';
10+
import { selectFolder } from '@/lib/nativeDialog';
1011
import { Button } from '@/components/ui/Button';
1112
import { Input } from '@/components/ui/Input';
1213
import {
@@ -69,7 +70,7 @@ function truncatePath(path: string, maxChars: number = 40): string {
6970
* Workspace selector component
7071
*
7172
* Provides a dropdown menu for selecting from recent workspace paths,
72-
* a manual path input dialog for entering custom paths, and delete buttons
73+
* a native OS folder picker, a manual path input dialog, and delete buttons
7374
* for removing paths from recent history.
7475
*
7576
* @example
@@ -86,15 +87,9 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
8687

8788
// UI state
8889
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
89-
const [isBrowseOpen, setIsBrowseOpen] = useState(false);
90+
const [isManualOpen, setIsManualOpen] = useState(false);
9091
const [manualPath, setManualPath] = useState('');
9192

92-
// Hidden file input for folder selection
93-
const folderInputRef = useRef<HTMLInputElement>(null);
94-
95-
/**
96-
* Handle path selection from dropdown
97-
*/
9893
const handleSelectPath = useCallback(
9994
async (path: string) => {
10095
await switchWorkspace(path);
@@ -103,77 +98,30 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
10398
[switchWorkspace]
10499
);
105100

106-
/**
107-
* Handle remove path from recent history
108-
*/
109101
const handleRemovePath = useCallback(
110102
async (e: React.MouseEvent, path: string) => {
111-
e.stopPropagation(); // Prevent triggering selection
103+
e.stopPropagation();
112104
await removeRecentPath(path);
113105
},
114106
[removeRecentPath]
115107
);
116108

117-
/**
118-
* Handle open folder browser - trigger hidden file input click
119-
*/
120-
const handleBrowseFolder = useCallback(() => {
109+
const handleBrowseFolder = useCallback(async () => {
121110
setIsDropdownOpen(false);
122-
// Trigger the hidden file input click
123-
folderInputRef.current?.click();
124-
}, []);
125-
126-
/**
127-
* Handle folder selection from file input
128-
*/
129-
const handleFolderSelect = useCallback(
130-
async (e: React.ChangeEvent<HTMLInputElement>) => {
131-
const files = e.target.files;
132-
if (files && files.length > 0) {
133-
// Get the path from the first file
134-
const firstFile = files[0];
135-
// The webkitRelativePath contains the full path relative to the selected folder
136-
// We need to get the parent directory path
137-
const relativePath = firstFile.webkitRelativePath;
138-
const folderPath = relativePath.substring(0, relativePath.indexOf('/'));
139-
140-
// In browser environment, we can't get the full absolute path
141-
// We need to ask the user to confirm or use the folder name
142-
// For now, open the manual dialog with the folder name as hint
143-
setManualPath(folderPath);
144-
setIsBrowseOpen(true);
145-
}
146-
// Reset input value to allow selecting the same folder again
147-
e.target.value = '';
148-
},
149-
[]
150-
);
111+
const selected = await selectFolder(projectPath || undefined);
112+
if (selected) {
113+
await switchWorkspace(selected);
114+
}
115+
}, [projectPath, switchWorkspace]);
151116

152-
/**
153-
* Handle manual path submission
154-
*/
155117
const handleManualPathSubmit = useCallback(async () => {
156118
const trimmedPath = manualPath.trim();
157-
if (!trimmedPath) {
158-
return; // TODO: Show validation error
159-
}
160-
119+
if (!trimmedPath) return;
161120
await switchWorkspace(trimmedPath);
162-
setIsBrowseOpen(false);
121+
setIsManualOpen(false);
163122
setManualPath('');
164123
}, [manualPath, switchWorkspace]);
165124

166-
/**
167-
* Handle dialog cancel
168-
*/
169-
const handleDialogCancel = useCallback(() => {
170-
setIsBrowseOpen(false);
171-
setManualPath('');
172-
}, []);
173-
174-
/**
175-
* Handle keyboard events in dialog input
176-
*/
177125
const handleInputKeyDown = useCallback(
178126
(e: React.KeyboardEvent<HTMLInputElement>) => {
179127
if (e.key === 'Enter') {
@@ -259,7 +207,7 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
259207

260208
{recentPaths.length > 0 && <DropdownMenuSeparator />}
261209

262-
{/* Browse button to open folder selector */}
210+
{/* Browse button to open native folder selector */}
263211
<DropdownMenuItem
264212
onClick={handleBrowseFolder}
265213
className="cursor-pointer gap-2"
@@ -279,7 +227,7 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
279227
<DropdownMenuItem
280228
onClick={() => {
281229
setIsDropdownOpen(false);
282-
setIsBrowseOpen(true);
230+
setIsManualOpen(true);
283231
}}
284232
className="cursor-pointer gap-2"
285233
>
@@ -290,20 +238,8 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
290238
</DropdownMenuContent>
291239
</DropdownMenu>
292240

293-
{/* Hidden file input for folder selection */}
294-
{/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
295-
<input
296-
ref={folderInputRef}
297-
type="file"
298-
{...({ webkitdirectory: '', directory: '' } as any)}
299-
style={{ display: 'none' }}
300-
onChange={handleFolderSelect}
301-
aria-hidden="true"
302-
tabIndex={-1}
303-
/>
304-
305241
{/* Manual path input dialog */}
306-
<Dialog open={isBrowseOpen} onOpenChange={setIsBrowseOpen}>
242+
<Dialog open={isManualOpen} onOpenChange={setIsManualOpen}>
307243
<DialogContent>
308244
<DialogHeader>
309245
<DialogTitle>
@@ -324,7 +260,10 @@ export function WorkspaceSelector({ className }: WorkspaceSelectorProps) {
324260
<DialogFooter>
325261
<Button
326262
variant="outline"
327-
onClick={handleDialogCancel}
263+
onClick={() => {
264+
setIsManualOpen(false);
265+
setManualPath('');
266+
}}
328267
>
329268
{formatMessage({ id: 'common.actions.cancel' })}
330269
</Button>
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* Native OS dialog helpers
3+
* Calls server-side endpoints that open system-native file/folder picker dialogs.
4+
*/
5+
6+
export async function selectFolder(initialDir?: string): Promise<string | null> {
7+
try {
8+
const res = await fetch('/api/dialog/select-folder', {
9+
method: 'POST',
10+
headers: { 'Content-Type': 'application/json' },
11+
body: JSON.stringify({ initialDir }),
12+
});
13+
if (!res.ok) return null;
14+
const data = await res.json();
15+
if (data.cancelled) return null;
16+
return data.path || null;
17+
} catch {
18+
return null;
19+
}
20+
}
21+
22+
export async function selectFile(initialDir?: string): Promise<string | null> {
23+
try {
24+
const res = await fetch('/api/dialog/select-file', {
25+
method: 'POST',
26+
headers: { 'Content-Type': 'application/json' },
27+
body: JSON.stringify({ initialDir }),
28+
});
29+
if (!res.ok) return null;
30+
const data = await res.json();
31+
if (data.cancelled) return null;
32+
return data.path || null;
33+
} catch {
34+
return null;
35+
}
36+
}

ccw/frontend/src/lib/unsplash.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export async function searchUnsplash(
5959
export async function uploadBackgroundImage(file: File): Promise<{ url: string; filename: string }> {
6060
const headers: Record<string, string> = {
6161
'Content-Type': file.type,
62-
'X-Filename': file.name,
62+
'X-Filename': encodeURIComponent(file.name),
6363
};
6464
const csrfToken = getCsrfToken();
6565
if (csrfToken) {

ccw/frontend/src/locales/en/workspace.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
"dialog": {
1313
"title": "Select Project Folder",
1414
"placeholder": "Enter project path...",
15-
"help": "The path to your project directory"
15+
"help": "The path to your project directory",
16+
"selectCurrent": "Select This Folder",
17+
"parentDir": "Parent Directory",
18+
"loading": "Loading...",
19+
"emptyDir": "Empty directory",
20+
"accessDenied": "Cannot access this directory"
1621
}
1722
},
1823
"actions": {

ccw/frontend/src/locales/zh/workspace.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@
1212
"dialog": {
1313
"title": "选择项目文件夹",
1414
"placeholder": "输入项目路径...",
15-
"help": "您的项目目录路径"
15+
"help": "您的项目目录路径",
16+
"selectCurrent": "选择此文件夹",
17+
"parentDir": "上级目录",
18+
"loading": "加载中...",
19+
"emptyDir": "空目录",
20+
"accessDenied": "无法访问此目录"
1621
}
1722
},
1823
"actions": {

0 commit comments

Comments
 (0)