Include workspace diff files in file typeahead results (Vibe Kanban) (#2083)
* The implementation is complete. Here's a summary of the changes made to `file-tag-typeahead-plugin.tsx`:
1. **Added import** for `useWorkspaceContext` from `@/contexts/WorkspaceContext`
2. **Created helper function** `getMatchingDiffFiles()` that:
- Takes a query and the `diffPaths` Set
- Filters paths that match the query (checking both filename and full path)
- Returns properly typed `DiffFileResult` objects with all required fields (`is_file`, `match_type`, `score`)
- Uses a high score value to ensure diff files rank above server results
3. **Modified `onQueryChange`** to:
- Extract `diffPaths` from WorkspaceContext
- Get matching local files from diffPaths first
- Fetch server results via `searchTagsAndFiles()`
- Deduplicate server file results by filtering out any paths already in local files
- Merge results with order: tags → local diff files → server files
The changes ensure that:
- Files from the current workspace diffs appear in typeahead results
- Diff files are ranked higher than server-sourced files
- No duplicate entries appear when a file exists in both sources
* Done. The fix involved two changes:
1. **`WorkspaceContext.tsx`** (line 78-81): Exported the `WorkspaceContext` so it can be used directly with `useContext`
2. **`file-tag-typeahead-plugin.tsx`**:
- Changed import from `useWorkspaceContext` to `WorkspaceContext`
- Added `useContext` to React imports
- Changed usage from `const { diffPaths } = useWorkspaceContext()` to:
```typescript
const workspaceContext = useContext(WorkspaceContext);
const diffPaths = workspaceContext?.diffPaths ?? new Set<string>();
```
Now when the WYSIWYG is used in the old UI (without a `WorkspaceProvider`), `workspaceContext` will be `null`, and `diffPaths` will default to an empty Set, meaning only server results will be shown. In the new UI with `WorkspaceProvider`, diff files will be included as expected.
* Done. The changes:
1. Added `MAX_FILE_RESULTS = 10` constant
2. Updated the result merging logic to:
- Limit local diff files to max 10
- Fill remaining slots with server files
- This ensures diffPaths files always appear first (prioritized)
File results are now capped at 10 total, preventing the menu from scrolling off the page.
* Cleanup script changes for workspace bda801c1-f48c-4ae8-948b-f0db6269d807
* Lint passes now. The fix was to wrap the `diffPaths` fallback in `useMemo` to ensure a stable reference when the context is not available.
This commit is contained in:
committed by
GitHub
parent
daa2690500
commit
010b03aafd
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useRef } from 'react';
|
import { useState, useCallback, useRef, useContext, useMemo } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from 'lexical';
|
} from 'lexical';
|
||||||
import { Tag as TagIcon, FileText } from 'lucide-react';
|
import { Tag as TagIcon, FileText } from 'lucide-react';
|
||||||
import { usePortalContainer } from '@/contexts/PortalContainerContext';
|
import { usePortalContainer } from '@/contexts/PortalContainerContext';
|
||||||
|
import { WorkspaceContext } from '@/contexts/WorkspaceContext';
|
||||||
import {
|
import {
|
||||||
searchTagsAndFiles,
|
searchTagsAndFiles,
|
||||||
type SearchResultItem,
|
type SearchResultItem,
|
||||||
@@ -33,6 +34,43 @@ const VIEWPORT_MARGIN = 8;
|
|||||||
const VERTICAL_GAP = 4;
|
const VERTICAL_GAP = 4;
|
||||||
const VERTICAL_GAP_ABOVE = 24;
|
const VERTICAL_GAP_ABOVE = 24;
|
||||||
const MIN_WIDTH = 320;
|
const MIN_WIDTH = 320;
|
||||||
|
const MAX_FILE_RESULTS = 10;
|
||||||
|
|
||||||
|
interface DiffFileResult {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
is_file: boolean;
|
||||||
|
match_type: 'FileName' | 'DirectoryName' | 'FullPath';
|
||||||
|
score: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMatchingDiffFiles(
|
||||||
|
query: string,
|
||||||
|
diffPaths: Set<string>
|
||||||
|
): DiffFileResult[] {
|
||||||
|
if (!query) return [];
|
||||||
|
const lowerQuery = query.toLowerCase();
|
||||||
|
return Array.from(diffPaths)
|
||||||
|
.filter((path) => {
|
||||||
|
const name = path.split('/').pop() || path;
|
||||||
|
return (
|
||||||
|
name.toLowerCase().includes(lowerQuery) ||
|
||||||
|
path.toLowerCase().includes(lowerQuery)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((path) => {
|
||||||
|
const name = path.split('/').pop() || path;
|
||||||
|
const nameMatches = name.toLowerCase().includes(lowerQuery);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
is_file: true,
|
||||||
|
match_type: nameMatches ? ('FileName' as const) : ('FullPath' as const),
|
||||||
|
// High score to rank diff files above server results
|
||||||
|
score: BigInt(Number.MAX_SAFE_INTEGER),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getMenuPosition(anchorEl: HTMLElement) {
|
function getMenuPosition(anchorEl: HTMLElement) {
|
||||||
const rect = anchorEl.getBoundingClientRect();
|
const rect = anchorEl.getBoundingClientRect();
|
||||||
@@ -73,6 +111,12 @@ export function FileTagTypeaheadPlugin({
|
|||||||
const [options, setOptions] = useState<FileTagOption[]>([]);
|
const [options, setOptions] = useState<FileTagOption[]>([]);
|
||||||
const lastMousePositionRef = useRef<{ x: number; y: number } | null>(null);
|
const lastMousePositionRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
const portalContainer = usePortalContainer();
|
const portalContainer = usePortalContainer();
|
||||||
|
// Use context directly to gracefully handle missing WorkspaceProvider (old UI)
|
||||||
|
const workspaceContext = useContext(WorkspaceContext);
|
||||||
|
const diffPaths = useMemo(
|
||||||
|
() => workspaceContext?.diffPaths ?? new Set<string>(),
|
||||||
|
[workspaceContext?.diffPaths]
|
||||||
|
);
|
||||||
|
|
||||||
const onQueryChange = useCallback(
|
const onQueryChange = useCallback(
|
||||||
(query: string | null) => {
|
(query: string | null) => {
|
||||||
@@ -82,16 +126,41 @@ export function FileTagTypeaheadPlugin({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get local diff files first (files from current workspace changes)
|
||||||
|
const localFiles = getMatchingDiffFiles(query, diffPaths);
|
||||||
|
const localFilePaths = new Set(localFiles.map((f) => f.path));
|
||||||
|
|
||||||
// Here query is a string, including possible empty string ''
|
// Here query is a string, including possible empty string ''
|
||||||
searchTagsAndFiles(query, { workspaceId, projectId })
|
searchTagsAndFiles(query, { workspaceId, projectId })
|
||||||
.then((results) => {
|
.then((serverResults) => {
|
||||||
setOptions(results.map((r) => new FileTagOption(r)));
|
// Separate tags and files from server results
|
||||||
|
const tagResults = serverResults.filter((r) => r.type === 'tag');
|
||||||
|
const serverFileResults = serverResults
|
||||||
|
.filter((r) => r.type === 'file')
|
||||||
|
.filter((r) => !localFilePaths.has(r.file!.path)); // Dedupe
|
||||||
|
|
||||||
|
// Limit total file results: prioritize local diff files
|
||||||
|
const limitedLocalFiles = localFiles.slice(0, MAX_FILE_RESULTS);
|
||||||
|
const remainingSlots = MAX_FILE_RESULTS - limitedLocalFiles.length;
|
||||||
|
const limitedServerFiles = serverFileResults.slice(0, remainingSlots);
|
||||||
|
|
||||||
|
// Build merged results: tags, then local files (ranked higher), then server files
|
||||||
|
const mergedResults: SearchResultItem[] = [
|
||||||
|
...tagResults,
|
||||||
|
...limitedLocalFiles.map((file) => ({
|
||||||
|
type: 'file' as const,
|
||||||
|
file,
|
||||||
|
})),
|
||||||
|
...limitedServerFiles,
|
||||||
|
];
|
||||||
|
|
||||||
|
setOptions(mergedResults.map((r) => new FileTagOption(r)));
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('Failed to search tags/files', err);
|
console.error('Failed to search tags/files', err);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[workspaceId, projectId]
|
[workspaceId, projectId, diffPaths]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ interface WorkspaceContextValue {
|
|||||||
diffStats: DiffStats;
|
diffStats: DiffStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
const WorkspaceContext = createContext<WorkspaceContextValue | null>(null);
|
// Exported for optional usage outside WorkspaceProvider (e.g., old UI)
|
||||||
|
export const WorkspaceContext = createContext<WorkspaceContextValue | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
interface WorkspaceProviderProps {
|
interface WorkspaceProviderProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
|||||||
Reference in New Issue
Block a user