diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index c01a2e379..4d0e8857c 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -642,50 +642,54 @@ export function AgentInputArea({ )} - {/* Command autocomplete dropdown */} - {autocomplete.isVisible && ( - - )} - {sendError && {sendError}} - {/* MessageInput handles everything: text, dictation, attachments, all buttons */} - { - setCursorIndex(selection.start) - }} - /> + + {/* Command + file mention autocomplete rendered as a true popover */} + {autocomplete.isVisible && ( + + + + )} + + {/* MessageInput handles everything: text, dictation, attachments, all buttons */} + { + setCursorIndex(selection.start) + }} + /> + @@ -709,7 +713,7 @@ const styles = StyleSheet.create(((theme: Theme) => ({ marginHorizontal: 'auto', alignItems: 'center', width: '100%', - overflow: 'hidden', + overflow: 'visible', padding: theme.spacing[4], }, inputAreaContent: { @@ -717,6 +721,18 @@ const styles = StyleSheet.create(((theme: Theme) => ({ maxWidth: MAX_CONTENT_WIDTH, gap: theme.spacing[3], }, + messageInputContainer: { + position: 'relative', + width: '100%', + }, + autocompletePopover: { + position: 'absolute', + left: 0, + right: 0, + bottom: '100%', + marginBottom: theme.spacing[3], + zIndex: 30, + }, cancelButton: { width: 34, height: 34, diff --git a/packages/app/src/components/ui/autocomplete.tsx b/packages/app/src/components/ui/autocomplete.tsx index 43d038628..e501cabbe 100644 --- a/packages/app/src/components/ui/autocomplete.tsx +++ b/packages/app/src/components/ui/autocomplete.tsx @@ -24,6 +24,16 @@ interface AutocompleteProps { maxHeight?: number } +const BOLT_GLYPH_PATTERN = /[\u26A1\uFE0F]/g + +function removeBoltGlyphs(value?: string): string | undefined { + if (!value) { + return value + } + const cleaned = value.replace(BOLT_GLYPH_PATTERN, '').trim() + return cleaned.length > 0 ? cleaned : undefined +} + export function Autocomplete({ options, selectedIndex, @@ -156,6 +166,9 @@ export function Autocomplete({ > {options.map((option, index) => { const isSelected = index === selectedIndex + const optionLabel = removeBoltGlyphs(option.label) ?? option.label + const optionDetail = removeBoltGlyphs(option.detail) + const optionDescription = removeBoltGlyphs(option.description) return ( - {option.label} - {option.detail ? {option.detail} : null} + {optionLabel} + {optionDetail ? {optionDetail} : null} - {option.description ? ( + {optionDescription ? ( - {option.description} + {optionDescription} ) : null} @@ -237,7 +250,7 @@ const styles = StyleSheet.create(((theme: Theme) => ({ itemLabel: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, - fontWeight: theme.fontWeight.medium, + fontWeight: theme.fontWeight.normal, }, itemDetail: { color: theme.colors.foregroundMuted, diff --git a/packages/app/src/hooks/use-agent-autocomplete.ts b/packages/app/src/hooks/use-agent-autocomplete.ts index 90552e93f..e9d72abd3 100644 --- a/packages/app/src/hooks/use-agent-autocomplete.ts +++ b/packages/app/src/hooks/use-agent-autocomplete.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from 'react' -import { useQuery } from '@tanstack/react-query' +import { keepPreviousData, useQuery } from '@tanstack/react-query' import type { AutocompleteOption } from '@/components/ui/autocomplete' import { useAgentCommandsQuery, type DraftCommandConfig } from './use-agent-commands-query' import { orderAutocompleteOptions } from '@/components/ui/autocomplete-utils' @@ -192,6 +192,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut isConnected, retry: false, staleTime: 15_000, + placeholderData: keepPreviousData, }) const options = useMemo(() => { @@ -214,11 +215,11 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut } if (mode === 'file' && activeFileMention) { - return (fileSuggestionsQuery.data ?? []).map((entry) => ({ + const orderedEntries = orderAutocompleteOptions(fileSuggestionsQuery.data ?? []) + return orderedEntries.map((entry) => ({ type: 'workspace_entry' as const, id: `${entry.kind}:${entry.path}`, label: entry.path, - detail: entry.kind === 'directory' ? 'Directory' : 'File', kind: entry.kind, entryPath: entry.path, mention: activeFileMention, @@ -260,7 +261,7 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut mode === 'command' ? isCommandsLoading : mode === 'file' - ? fileSuggestionsQuery.isPending || fileSuggestionsQuery.isLoading + ? fileSuggestionsQuery.isPending || (fileSuggestionsQuery.isLoading && options.length === 0) : false const errorMessage = mode === 'command' diff --git a/packages/server/src/utils/directory-suggestions.test.ts b/packages/server/src/utils/directory-suggestions.test.ts index 2177ee17b..09a9104b7 100644 --- a/packages/server/src/utils/directory-suggestions.test.ts +++ b/packages/server/src/utils/directory-suggestions.test.ts @@ -234,4 +234,33 @@ describe('searchWorkspaceEntries', () => { }) expect(escapedResults.some((entry) => entry.path.includes('escaped-link'))).toBe(false) }) + + it('ignores node_modules entries so deep workspace files still resolve under scan limits', async () => { + mkdirSync(path.join(workspaceDir, 'packages', 'app', 'src', 'app'), { recursive: true }) + writeFileSync(path.join(workspaceDir, 'packages', 'app', 'src', 'app', '_layout.tsx'), '') + + for (let index = 0; index < 120; index += 1) { + mkdirSync(path.join(workspaceDir, 'node_modules', `pkg-${index}`), { recursive: true }) + writeFileSync( + path.join(workspaceDir, 'node_modules', `pkg-${index}`, 'index.js'), + 'module.exports = {};\n' + ) + } + writeFileSync(path.join(workspaceDir, 'node_modules', '_layout.tsx'), '') + + const results = await searchWorkspaceEntries({ + cwd: workspaceDir, + query: '_layout.tsx', + limit: 20, + includeFiles: true, + includeDirectories: true, + maxEntriesScanned: 60, + }) + + expect(results).toContainEqual({ + path: 'packages/app/src/app/_layout.tsx', + kind: 'file', + }) + expect(results.some((entry) => entry.path.startsWith('node_modules/'))).toBe(false) + }) }) diff --git a/packages/server/src/utils/directory-suggestions.ts b/packages/server/src/utils/directory-suggestions.ts index 2298e28b6..c2ad7d354 100644 --- a/packages/server/src/utils/directory-suggestions.ts +++ b/packages/server/src/utils/directory-suggestions.ts @@ -73,6 +73,7 @@ const directoryListCache = new Map() const workspaceEntryListCache = new Map() const NO_SEGMENT_INDEX = Number.MAX_SAFE_INTEGER const NO_MATCH_OFFSET = Number.MAX_SAFE_INTEGER +const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set(['node_modules']) export async function searchHomeDirectories( options: SearchHomeDirectoriesOptions @@ -750,6 +751,9 @@ async function listWorkspaceChildEntries(input: { if (isHiddenDirectoryName(dirent.name)) { continue } + if (isIgnoredWorkspaceDirectoryName(dirent.name)) { + continue + } const candidatePath = path.join(input.directory, dirent.name) const entry = await resolveWorkspaceCandidate({ @@ -839,6 +843,10 @@ function isHiddenDirectoryName(name: string): boolean { return name.startsWith('.') } +function isIgnoredWorkspaceDirectoryName(name: string): boolean { + return WORKSPACE_IGNORED_DIRECTORY_NAMES.has(name) +} + function setDirectoryListCache(cacheKey: string, entry: DirectoryListCacheEntry): void { directoryListCache.set(cacheKey, entry) pruneDirectoryListCache()