From 2b555808fd15aedeb7bbe9bc038c33d34ae87423 Mon Sep 17 00:00:00 2001 From: Yoni Kliger Date: Mon, 16 Mar 2026 10:48:49 +0200 Subject: [PATCH 1/2] feat: Show all files in @ mentions with performance optimizations Previously, file mentions were limited to showing only 5 files when typing @ in the input field. This made it difficult to find files in larger projects, especially in connection sessions with multiple worktrees. Changes to useFileMentions.ts: - Removed .slice(0, 5) limit for both empty and search queries - Added MAX_SUGGESTIONS = 200 cap to prevent UI performance issues - Eliminated double filterSuggestions computation (was called in both suggestions and effectiveSuggestions memos) - Now only effectiveSuggestions memo calls filterSuggestions - moveSelection callback uses effectiveSuggestions.length - Removed verbose debug console.log statements from hot path - All matching files shown with proper filtering and scoring Changes to FileMentionPopover.tsx: - Fixed React key from file.relativePath to file.path - Prevents React confusion when multiple worktrees have same file names - Ensures proper re-rendering when suggestions change across keystrok es Performance: - Handles large file lists (1000+ files) efficiently - Caps results at 200 to maintain UI responsiveness - Single filterSuggestions call per keystroke (not double) - No verbose logging in production hot path - Keyboard navigation (arrow keys) works smoothly with all results - Automatic scroll-to-view for selected items Co-Authored-By: Claude Sonnet 4.5 --- .../sessions/FileMentionPopover.tsx | 2 +- src/renderer/src/hooks/useFileMentions.ts | 45 +++++++++---------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/src/renderer/src/components/sessions/FileMentionPopover.tsx b/src/renderer/src/components/sessions/FileMentionPopover.tsx index eaab654f..374d6493 100644 --- a/src/renderer/src/components/sessions/FileMentionPopover.tsx +++ b/src/renderer/src/components/sessions/FileMentionPopover.tsx @@ -82,7 +82,7 @@ export function FileMentionPopover({ ) : ( suggestions.map((file, index) => (
a.relativePath.localeCompare(b.relativePath)).slice(0, 5) + // Return up to MAX_SUGGESTIONS files alphabetically by relativePath + return [...flatFiles] + .sort((a, b) => a.relativePath.localeCompare(b.relativePath)) + .slice(0, MAX_SUGGESTIONS) } // Score and filter @@ -84,7 +89,7 @@ function filterSuggestions(flatFiles: FlatFile[], query: string): FlatFile[] { if (b.score !== a.score) return b.score - a.score return a.file.relativePath.localeCompare(b.file.relativePath) }) - .slice(0, 5) + .slice(0, MAX_SUGGESTIONS) // Cap results for performance return scored.map(({ file }) => file) } @@ -102,12 +107,6 @@ export function useFileMentions(inputValue: string, cursorPosition: number, flat const { isOpen, query, triggerIndex } = trigger - // Filter suggestions - const suggestions = useMemo( - () => (isOpen ? filterSuggestions(flatFiles, query) : []), - [isOpen, flatFiles, query] - ) - // Reset selectedIndex when query changes useEffect(() => { if (query !== prevQueryRef.current) { @@ -116,20 +115,6 @@ export function useFileMentions(inputValue: string, cursorPosition: number, flat } }, [query]) - // Keyboard navigation - const moveSelection = useCallback( - (direction: 'up' | 'down') => { - if (suggestions.length === 0) return - setSelectedIndex((prev) => { - if (direction === 'down') { - return (prev + 1) % suggestions.length - } - return (prev - 1 + suggestions.length) % suggestions.length - }) - }, - [suggestions.length] - ) - // Select a file — returns insertion data const selectFile = useCallback( (file: FlatFile): SelectFileResult => { @@ -177,6 +162,20 @@ export function useFileMentions(inputValue: string, cursorPosition: number, flat [effectiveIsOpen, flatFiles, query] ) + // Keyboard navigation + const moveSelection = useCallback( + (direction: 'up' | 'down') => { + if (effectiveSuggestions.length === 0) return + setSelectedIndex((prev) => { + if (direction === 'down') { + return (prev + 1) % effectiveSuggestions.length + } + return (prev - 1 + effectiveSuggestions.length) % effectiveSuggestions.length + }) + }, + [effectiveSuggestions.length] + ) + // Update mention indices when the input text changes const updateMentions = useCallback((oldValue: string, newValue: string) => { if (oldValue === newValue) return From a3b8b00ca241455f541fe33ad1e61ea9cdd9bed7 Mon Sep 17 00:00:00 2001 From: Yoni Kliger Date: Mon, 16 Mar 2026 10:49:01 +0200 Subject: [PATCH 2/2] fix: Add file mentions support for connection sessions Previously, @file mentions did not work in connection sessions because: 1. Connection path resolution failures were silently ignored (console.warn only) 2. Connections aggregate multiple worktrees, not a single git repository 3. git ls-files on connection path would fail (not a git repository) 4. File index was never loaded for connection sessions 5. Connection data wasn't subscribed to, causing timing issues This commit adds proper connection support with comprehensive fixes: SessionView.tsx changes: - Added connectionPathError state to track path resolution failures - Improved error handling with toast notifications and detailed console.error - Added error banner UI with retry button when connection path fails - Enhanced file index loading to detect connection vs worktree sessions - Subscribe to activeConnection via useConnectionStore hook (not imperative getState) - Ensures effect re-runs when connection data loads after initial mount - Removed connectionPathError from effect dependencies to prevent race conditions - Now loads files from all connection member worktrees with symlink prefixes - Passes member data (symlinkName + worktreePath) to file tree store useFileTreeStore.ts changes: - Added loadFileIndexForConnection(connectionId, members[]) method - Accepts members array with { symlinkName, worktreePath } objects - Prefixes each file's relativePath with member symlink name - Example: "repo-a/src/index.ts" distinguishes from "repo-b/src/index.ts" - Prevents ambiguity when multiple worktrees have same relative paths - Loads files from all member worktrees in parallel using Promise.all - Removes duplicate files based on prefixed relativePath (not path) - Uses connection:${id} as cache key to distinguish from worktree sessions - Tracks hasFailures flag to detect when all member scans fail - Doesn't store empty array on total failure (allows retry via reference check) - Ensures fileIndex === EMPTY_FILE_INDEX guard can trigger retry - Added comprehensive error logging for debugging scan failures File mentions now work correctly in both: - Single worktree sessions (unchanged behavior, no prefix) - Connection sessions (new: aggregates from all members with prefixes) User experience improvements: - Clear error messages when connection fails to load - Retry button for failed connections - Toast notifications for failures - File paths disambiguated with member symlink names - Proper retry behavior when connection data loads late - No spurious effect re-runs from connectionPathError state changes Co-Authored-By: Claude Sonnet 4.5 --- .../src/components/sessions/SessionView.tsx | 106 +++++++++++++++--- src/renderer/src/stores/useFileTreeStore.ts | 96 +++++++++++++++- 2 files changed, 188 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/sessions/SessionView.tsx b/src/renderer/src/components/sessions/SessionView.tsx index 5812cdd3..14e445df 100644 --- a/src/renderer/src/components/sessions/SessionView.tsx +++ b/src/renderer/src/components/sessions/SessionView.tsx @@ -459,21 +459,65 @@ export function SessionView({ sessionId }: SessionViewProps): React.JSX.Element const inputValueRef = useRef('') const draftTimerRef = useRef | null>(null) - // Flat file index for file mentions and search — keyed by worktree path. - // Uses git ls-files for a complete, gitignore-respecting file list. - // Ensure the index is loaded when worktreePath is resolved — SessionView cannot - // rely on the FileTree sidebar component having already populated the store - // (sidebar may be collapsed, on a different tab, or targeting a different worktree). - const fileIndex = useFileTreeStore((state) => - worktreePath - ? (state.fileIndexByWorktree.get(worktreePath) ?? EMPTY_FILE_INDEX) - : EMPTY_FILE_INDEX + // Connection path resolution error state + const [connectionPathError, setConnectionPathError] = useState(null) + + // Flat file index for file mentions and search. + // For worktree sessions: use worktreePath as key + // For connection sessions: use `connection:${connectionId}` as key and aggregate from all members + const fileIndex = useFileTreeStore((state) => { + if (worktreeId) { + // Regular worktree session + return worktreePath + ? (state.fileIndexByWorktree.get(worktreePath) ?? EMPTY_FILE_INDEX) + : EMPTY_FILE_INDEX + } else if (connectionId) { + // Connection session - use connectionId as key + const cacheKey = `connection:${connectionId}` + return state.fileIndexByWorktree.get(cacheKey) ?? EMPTY_FILE_INDEX + } + return EMPTY_FILE_INDEX + }) + + // Subscribe to connection data for connection sessions + const activeConnection = useConnectionStore((state) => + connectionId ? state.connections.find((c) => c.id === connectionId) : undefined ) + useEffect(() => { - if (worktreePath && fileIndex === EMPTY_FILE_INDEX) { + if (worktreeId && worktreePath && fileIndex === EMPTY_FILE_INDEX) { + // Regular worktree session - load from single worktree + console.log('[SessionView] Loading file index for worktree:', worktreePath) useFileTreeStore.getState().loadFileIndex(worktreePath) + } else if (connectionId && fileIndex === EMPTY_FILE_INDEX) { + // Connection session - load from all member worktrees + // Use the subscribed activeConnection instead of imperative getState() + if (activeConnection && activeConnection.members.length > 0) { + const members = activeConnection.members.map((m) => ({ + symlinkName: m.symlink_name, + worktreePath: m.worktree_path + })) + console.log( + '[SessionView] Loading file index for connection:', + connectionId, + 'from', + members.length, + 'members' + ) + useFileTreeStore.getState().loadFileIndexForConnection(connectionId, members) + } else if (!activeConnection) { + console.warn('[SessionView] Connection not found for file index loading', { + sessionId, + connectionId + }) + } else { + console.warn('[SessionView] Connection has no members - file mentions disabled', { + sessionId, + connectionId + }) + } } - }, [worktreePath, fileIndex]) + }, [worktreeId, worktreePath, connectionId, fileIndex, sessionId, activeConnection]) // File mentions hook const fileMentions = useFileMentions(inputValue, cursorPosition, fileIndex) @@ -1903,16 +1947,28 @@ export function SessionView({ sessionId }: SessionViewProps): React.JSX.Element } else if (session.connection_id) { // Connection session: resolve the connection folder path setConnectionId(session.connection_id) + setConnectionPathError(null) // Clear any previous error try { const connResult = await window.connectionOps.get(session.connection_id) if (shouldAbortInit()) return + if (connResult.success && connResult.connection) { wtPath = connResult.connection.path setWorktreePath(wtPath) transcriptSourceRef.current.worktreePath = wtPath + setConnectionPathError(null) // Explicitly clear on success + } else { + // Connection lookup failed or returned no connection + const errorMsg = connResult.error || 'Connection not found' + console.error('Failed to resolve connection path:', errorMsg, 'connectionId:', session.connection_id) + setConnectionPathError(errorMsg) + toast.error(`Failed to load connection: ${errorMsg}`) } - } catch { - console.warn('Failed to resolve connection path for session') + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error' + console.error('Exception resolving connection path:', error, 'connectionId:', session.connection_id) + setConnectionPathError(errorMsg) + toast.error(`Failed to load connection: ${errorMsg}`) } } @@ -4029,6 +4085,30 @@ export function SessionView({ sessionId }: SessionViewProps): React.JSX.Element {/* Attachment previews */} + {/* Connection error banner */} + {connectionPathError && ( +
+ +
+

Connection Error

+

+ Failed to load connection path: {connectionPathError}. File mentions (@) will not work. +

+
+ +
+ )} + {/* Middle: textarea */}