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()