feat(mpp-ui): implement file context management with indexed search#36
feat(mpp-ui): implement file context management with indexed search#36
Conversation
- Add SelectedFileItem, FileChip, TopToolbar, FileSearchPopup components - Add WorkspaceFileSearchProvider with pre-built file index for fast search - Add IndexingState enum for tracking indexing progress - Integrate file context into DevInEditorInput with buildAndSendMessage() - Add Prompt Enhancement button to BottomToolbar - Add AutoAwesome and History icons to AutoDevComposeIcons - Add FileContext to EditorCallbacks for file context submission Closes #35
WalkthroughThe PR implements file context management for the multiplatform Compose UI, enabling users to select and submit files alongside text input. It adds new UI components for file selection and management, modifies core callbacks to support file contexts, and integrates workspace-based file searching with in-memory indexing. Changes
Sequence DiagramsequenceDiagram
actor User
participant DevInEditorInput
participant TopToolbar
participant FileSearchPopup
participant WorkspaceFileSearchProvider
participant EditorCallbacks
User->>DevInEditorInput: Types message + clicks add-file
DevInEditorInput->>TopToolbar: Render file manager toolbar
TopToolbar->>FileSearchPopup: Open on add-file click
User->>FileSearchPopup: Types search query
FileSearchPopup->>WorkspaceFileSearchProvider: searchFiles(query)
alt First search
WorkspaceFileSearchProvider->>WorkspaceFileSearchProvider: buildIndex() - scan workspace
end
WorkspaceFileSearchProvider-->>FileSearchPopup: Return filtered results
User->>FileSearchPopup: Selects file
FileSearchPopup->>TopToolbar: onSelectFile(SelectedFileItem)
TopToolbar->>DevInEditorInput: Update selectedFiles state
User->>DevInEditorInput: Presses Enter or clicks submit
DevInEditorInput->>DevInEditorInput: buildAndSendMessage()
Note over DevInEditorInput: Assemble text + per-file<br/>DevIns commands
DevInEditorInput->>EditorCallbacks: onSubmit(text, files: List<FileContext>)
EditorCallbacks-->>User: Process submission with file context
DevInEditorInput->>DevInEditorInput: Reset input & selections
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Areas requiring extra attention:
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
| private val _indexingState = MutableStateFlow(IndexingState.NOT_STARTED) | ||
| val indexingState: StateFlow<IndexingState> = _indexingState | ||
|
|
||
| private var fileIndex: List<IndexedFile> = emptyList() |
There was a problem hiding this comment.
fileIndex and indexedWorkspacePath are shared mutable state accessed from multiple coroutines (e.g., buildIndex and searchFiles) without synchronization, which can cause races/visibility issues; consider guarding with a Mutex or confining updates/reads to a single dispatcher/thread. (Guideline: thread_safety)
🤖 Was this useful? React with 👍 or 👎
| var autoAddCurrentFile by remember { mutableStateOf(true) } | ||
|
|
||
| // File search provider - use WorkspaceFileSearchProvider as default if not provided | ||
| val effectiveSearchProvider = remember { fileSearchProvider ?: WorkspaceFileSearchProvider() } |
There was a problem hiding this comment.
effectiveSearchProvider is remembered without a key, so changes to the fileSearchProvider parameter won’t be observed and can lead to a stale provider; consider remember(fileSearchProvider) { ... } to react to updates.
🤖 Was this useful? React with 👍 or 👎
|
|
||
| actual override fun searchFiles(pattern: String, maxDepth: Int, maxResults: Int): List<String> { | ||
| return try { | ||
| println("[DefaultFileSystem] searchFiles called: pattern=$pattern, projectPath=$projectPath") |
There was a problem hiding this comment.
Debug logging via println includes projectPath and emits stack traces; consider using the project’s logger with appropriate levels and avoiding potentially sensitive path details in logs (also applies to other printlns in this method). (Guideline: no_sensitive_logging)
🤖 Was this useful? React with 👍 or 👎
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (7)
mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt (3)
108-113: Consoleprintlndebug logging should be temporary or behind a logger/config flagThe added
printlncalls ande.printStackTrace()are useful for debugging but will be noisy in normal runs and hard to control in library usage. Consider switching to a proper logging abstraction (or injecting a logger) and/or guarding these under a debug/config flag so callers can control verbosity.Also applies to: 141-145, 187-193
115-133: Custom glob → regex conversion is non‑trivial; consider hardening or centralizingThe placeholder-based conversion for
**/,**,*,?, and{a,b}looks reasonable for common patterns (**/*.kt,*.md, etc.), but:
- Other regex metacharacters (
[ ] ( ) + ^ $ | \) in patterns aren’t escaped and could produce surprising matches orPatternSyntaxExceptions.- This logic is subtle and easy to regress as new pattern shapes arise.
If you expect broader glob usage, consider either:
- Centralizing this into a tested utility with dedicated unit tests for tricky patterns, or
- Delegating to JDK glob support (e.g.,
FileSystems.getDefault().getPathMatcher("glob:$pattern")) and adapting the matching strategy, to reduce custom parsing surface.
136-143: Hard‑codedbuildexclusion and per‑call gitignore reload may not always be desirableTwo behavioral points to double‑check:
criticalExcludeDirs = setOf(".git", "build")now permanently excludes any files under abuilddirectory, regardless of.gitignore. This matches your test expectations but may surprise users in repos wherebuildcontains meaningful sources. Consider making this configurable or relying on.gitignorefor non‑critical dirs.gitIgnoreParser?.reload()on everysearchFilescall guarantees fresh patterns but may be unnecessary overhead if searches are triggered frequently (e.g., on each keypress in a UI). You might cache until explicit invalidation or add a cheap “reload if changed” mechanism.Both are fine as pragmatic choices for now, but worth revisiting if search is on a hot path.
Also applies to: 155-162
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt (1)
7-12: Consider reviewing the default value forrelativePath.The default
relativePath = nameseems semantically incorrect. A relative path should typically default topathor be derived from it, not fromname(which is just the filename). If the caller doesn't providerelativePath, using the filename alone may cause issues when the path context matters.data class FileContext( val name: String, val path: String, - val relativePath: String = name, + val relativePath: String = path, val isDirectory: Boolean = false )mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt (1)
96-125: Missing layout case when expanded with selected files.When
isExpandedis true andselectedFiles.isNotEmpty(), neither the file chips row (line 96) nor the spacer (line 124) is rendered in the main toolbar row. This leaves noweight(1f)modifier in the row, which could cause layout issues with the right-side buttons.Consider adding an else branch or restructuring:
} - } else if (selectedFiles.isEmpty()) { + } else { + // Either empty files or expanded mode - use spacer for layout Spacer(modifier = Modifier.weight(1f)) }mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt (1)
28-39: Duplicated truncation logic withtruncatePathfunction.The
truncatedPathproperty (lines 28-39) and the standalonetruncatePathfunction (lines 64-75) implement similar but inconsistent logic: different max lengths (40 vs 30), different parts kept (4 vs 3), and inconsistent prefix formatting (...vs.../). Consider consolidating into a single reusable implementation.val truncatedPath: String get() { - val parentPath = relativePath.substringBeforeLast("/", "") - if (parentPath.isEmpty()) return "" - if (parentPath.length <= 40) return parentPath - - val parts = parentPath.split("/") - if (parts.size <= 2) return "...$parentPath" - - val keepParts = parts.takeLast(4) - return "...${keepParts.joinToString("/")}" + return truncatePath(relativePath, maxLength = 40) }mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt (1)
191-201: Thread-safety concern inInMemoryRecentFilesProvider.The
recentFileslist uses a non-thread-safemutableListOf. IfaddRecentFileandgetRecentFilesare called from different threads, this could cause aConcurrentModificationException.Consider using a thread-safe collection or synchronization:
class InMemoryRecentFilesProvider(private val maxSize: Int = 20) : RecentFilesProvider { - private val recentFiles = mutableListOf<SelectedFileItem>() + private val recentFiles = java.util.Collections.synchronizedList(mutableListOf<SelectedFileItem>()) - override suspend fun getRecentFiles(): List<SelectedFileItem> = recentFiles.toList() + override suspend fun getRecentFiles(): List<SelectedFileItem> = synchronized(recentFiles) { recentFiles.toList() } - override fun addRecentFile(file: SelectedFileItem) { - recentFiles.removeAll { it.path == file.path } - recentFiles.add(0, file) - if (recentFiles.size > maxSize) recentFiles.removeAt(recentFiles.lastIndex) + override fun addRecentFile(file: SelectedFileItem) = synchronized(recentFiles) { + recentFiles.removeAll { it.path == file.path } + recentFiles.add(0, file) + if (recentFiles.size > maxSize) recentFiles.removeAt(recentFiles.lastIndex) } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt(2 hunks)mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt(3 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt(2 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt(9 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt(1 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt(1 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt(1 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt(1 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt(1 hunks)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.kt(1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.kt
📄 CodeRabbit inference engine (AGENTS.md)
**/*.kt: Useexpect/actualfor platform-specific code (e.g., file I/O on JVM/JS/Wasm) in Kotlin Multiplatform projects
Check export first if some functions are not working well with CLI (TypeScript)
In Kotlin/JS @JsExport: AvoidFlow, usePromiseinstead
In Kotlin/JS @JsExport: Use concrete classes as return types and parameter types; avoid interface types
For WASM platform, avoid using emoji and UTF-8 in code
Files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.ktmpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.ktmpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt
**/compose/**/*.{kt,kts}
📄 CodeRabbit inference engine (AGENTS.md)
In Compose (Desktop/Android), use
AutoDevColorsfromcc.unitmesh.devins.ui.compose.themeorMaterialTheme.colorScheme
Files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt
{**/compose/**/*.kt,mpp-ui/src/jsMain/typescript/**/*.{ts,tsx}}
📄 CodeRabbit inference engine (AGENTS.md)
DO NOT hardcode colors (e.g.,
Color(0xFF...)or#hex). Always use design tokens for consistency across platforms
Files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt
🧠 Learnings (3)
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/compose/**/*.{kt,kts} : In Compose (Desktop/Android), use `AutoDevColors` from `cc.unitmesh.devins.ui.compose.theme` or `MaterialTheme.colorScheme`
Applied to files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.ktmpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/idea/**/*.kt : For popup/dropdown menus in IntelliJ plugins, use Jewel's `PopupMenu` instead of `androidx.compose.ui.window.Popup`
Applied to files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
📚 Learning: 2025-12-02T00:20:34.480Z
Learnt from: CR
Repo: phodal/auto-dev-sketch PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-02T00:20:34.480Z
Learning: Applies to **/idea/**/*.kt : For SwingPanel z-index issues with Compose popups, enable Jewel's custom popup renderer: `JewelFlags.useCustomPopupRenderer = true` in `IdeaAgentToolWindowFactory`
Applied to files:
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
🧬 Code graph analysis (1)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt (2)
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt (1)
FileSearchPopup(49-239)mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt (2)
FileChip(29-104)FileChipExpanded(110-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Agent
- GitHub Check: Build (241)
- GitHub Check: Build (223)
- GitHub Check: Test mpp-core (KMP)
🔇 Additional comments (22)
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt (1)
26-32: LGTM!The overloaded
onSubmitmethod with file context properly defaults to the existingonSubmit(text)behavior, maintaining backward compatibility for implementations that don't need file context support.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.kt (1)
62-62: LGTM!The
AutoAwesomeicon addition follows the established pattern and is properly placed in the "Communication & AI" section alongside related icons.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt (2)
28-29: LGTM!New parameters follow the existing pattern and maintain backward compatibility with default values.
154-174: LGTM!The Prompt Enhancement button correctly implements a loading state pattern with
CircularProgressIndicatorwhen enhancing, and properly disables the button during the operation. All colors are sourced fromMaterialTheme.colorScheme, complying with the coding guidelines.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt (1)
189-222: LGTM!The
ContextIndicatorcomponent correctly uses theme colors for active/inactive states and provides clear accessibility descriptions. The active indicator dot overlay is a nice touch for visual feedback.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt (3)
91-101: Consider touch accessibility for mobile platforms.The remove button is only visible on hover, which works on desktop but not on touch devices. Since the codebase targets multiple platforms (Android, iOS per
Platform.isAndroid || Platform.isIOSchecks elsewhere), consider whether mobile users need an alternative way to remove files, such as a long-press gesture or always-visible button on mobile.
29-104: LGTM!The
FileChipcomponent properly implements hover-based interactions matching IDEA behavior. All colors are derived fromMaterialTheme.colorScheme, complying with the coding guidelines. The combination ofclip,hoverable, andbordermodifiers is correctly ordered for proper visual effects.
110-173: LGTM!The
FileChipExpandedcomponent correctly provides a fuller representation with the remove button always visible (with hover-based tint adjustment). The layout withweight(1f)on the file info column ensures proper text truncation while maintaining the remove button's position.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt (1)
46-57: LGTM!The
fromPathfactory method correctly handles both Unix and Windows path separators with appropriate fallbacks.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt (5)
104-109: LGTM!File context state and search provider initialization are well-structured. The
rememberblock correctly memoizes the effective provider, and the fallback toWorkspaceFileSearchProviderprovides sensible defaults.
121-139: LGTM!The
buildAndSendMessagefunction correctly constructs the full message with DevIns file commands and properly clears state after submission.
491-504: LGTM!TopToolbar integration is correctly scoped to desktop platforms with proper state management for file selection.
674-675: LGTM!The enhance button is correctly wired to the
enhanceCurrentInputfunction with proper state feedback.
315-315: Consistent message submission.All send paths (keyboard, IME action, button click, auto-execute) now correctly use
buildAndSendMessage, ensuring file contexts are always included.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt (4)
27-37: LGTM!The
FileSearchProviderinterface provides a clean abstraction for platform-specific implementations, andDefaultFileSearchProvideroffers a sensible no-op fallback.
371-377: Color usage complies with guidelines.All colors correctly use
MaterialTheme.colorSchemetokens rather than hardcoded values. Based on coding guidelines.
262-293: LGTM!The
SearchFieldcomponent properly handles keyboard events and uses theme-compliant styling.
323-378: LGTM!The
FileMenuItemcomponent implements hover behavior correctly with appropriate icon selection for different file types.mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt (4)
16-21: LGTM!The
IndexingStateenum provides clear state representation for the indexing lifecycle.
86-119: LGTM!The recursive indexing correctly implements depth and size limits to prevent unbounded traversal, with sensible exclusions for hidden files and build directories.
121-153: LGTM!The search implementation correctly triggers index building when needed, filters efficiently, and sorts files before directories to match the UI grouping.
176-181: LGTM!The
IGNORED_DIRSset covers common build artifacts and IDE directories appropriately.
| // Debounced search function | ||
| fun performSearch(query: String) { | ||
| if (query.length < 2 || currentWorkspace == null || indexingState != IndexingState.READY) { | ||
| searchResults = emptyList() | ||
| isLoading = false | ||
| return | ||
| } | ||
|
|
||
| isLoading = true | ||
| scope.launch { | ||
| delay(150) // Debounce | ||
| try { | ||
| searchResults = searchProvider.searchFiles(query) | ||
| } catch (e: Exception) { | ||
| searchResults = emptyList() | ||
| } finally { | ||
| isLoading = false | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential race condition in debounced search.
The performSearch function launches a new coroutine on each call without canceling the previous one. Rapid typing could cause older searches to complete after newer ones, displaying stale results.
Consider tracking and canceling the previous search job:
+ var searchJob by remember { mutableStateOf<kotlinx.coroutines.Job?>(null) }
+
// Debounced search function
fun performSearch(query: String) {
if (query.length < 2 || currentWorkspace == null || indexingState != IndexingState.READY) {
searchResults = emptyList()
isLoading = false
return
}
isLoading = true
+ searchJob?.cancel()
- scope.launch {
+ searchJob = scope.launch {
delay(150) // Debounce
try {
searchResults = searchProvider.searchFiles(query)
} catch (e: Exception) {
searchResults = emptyList()
} finally {
isLoading = false
}
}
}🤖 Prompt for AI Agents
In
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
around lines 94 to 113, the debounced performSearch launches a new coroutine
each call causing race conditions; fix by storing the launched coroutine in a
cancellable Job property (e.g., private var searchJob: Job? = null), cancel the
previous searchJob?.cancel() before launching a new one, assign the newly
launched coroutine to searchJob, and keep the existing delay/try/catch/finally
logic so older tasks are stopped and only the latest search result updates
state.
| fun truncatePath(path: String, maxLength: Int = 30): String { | ||
| val parentPath = path.substringBeforeLast('/') | ||
| if (parentPath.isEmpty() || parentPath == path) return "" | ||
|
|
||
| if (parentPath.length <= maxLength) return parentPath | ||
|
|
||
| val parts = parentPath.split('/') | ||
| if (parts.size <= 2) return "...$parentPath" | ||
|
|
||
| val keepParts = parts.takeLast(3) | ||
| return ".../${keepParts.joinToString("/")}" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n --type=kt '\btruncatePath\s*\(' --glob='!**/SelectedFileItem.kt'Repository: phodal/auto-dev-sketch
Length of output: 92
🏁 Script executed:
rg -n 'truncatePath' --glob='*.kt'Repository: phodal/auto-dev-sketch
Length of output: 224
Remove unused truncatePath function.
This function is not called anywhere in the codebase and should be deleted.
🤖 Prompt for AI Agents
In
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt
around lines 64 to 75, the truncatePath function is unused and should be
removed; delete the entire function definition (including its signature and
body) and then run a quick project-wide search/build to ensure there are no
remaining references or required imports to clean up.
| suspend fun buildIndex() = withContext(Dispatchers.Default) { | ||
| val workspace = WorkspaceManager.currentWorkspace | ||
| if (workspace == null) { | ||
| AutoDevLogger.warn(TAG) { "buildIndex: No workspace available" } | ||
| return@withContext | ||
| } | ||
|
|
||
| val rootPath = workspace.rootPath | ||
| if (rootPath == null) { | ||
| AutoDevLogger.warn(TAG) { "buildIndex: No root path available" } | ||
| return@withContext | ||
| } | ||
|
|
||
| AutoDevLogger.info(TAG) { "buildIndex: workspace=$rootPath, currentState=${_indexingState.value}" } | ||
|
|
||
| // Skip if already indexed for this workspace | ||
| if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty()) { | ||
| AutoDevLogger.info(TAG) { "buildIndex: Already indexed ${fileIndex.size} files" } | ||
| _indexingState.value = IndexingState.READY | ||
| return@withContext | ||
| } | ||
|
|
||
| _indexingState.value = IndexingState.INDEXING | ||
| AutoDevLogger.info(TAG) { "buildIndex: Starting indexing..." } | ||
|
|
||
| try { | ||
| val files = mutableListOf<IndexedFile>() | ||
| indexFilesRecursively(workspace, "", files, maxDepth = 6) | ||
| fileIndex = files | ||
| indexedWorkspacePath = rootPath | ||
| _indexingState.value = IndexingState.READY | ||
| AutoDevLogger.info(TAG) { "Index built: ${files.size} files" } | ||
| } catch (e: Exception) { | ||
| AutoDevLogger.error(TAG) { "Index error: ${e.message}" } | ||
| e.printStackTrace() | ||
| _indexingState.value = IndexingState.ERROR | ||
| } | ||
| } |
There was a problem hiding this comment.
Potential race condition in concurrent buildIndex calls.
If buildIndex is called concurrently (e.g., rapid popup open/close), multiple coroutines could pass the "already indexed" check before any sets INDEXING state, leading to redundant indexing work.
Consider adding a guard at the start:
suspend fun buildIndex() = withContext(Dispatchers.Default) {
+ // Prevent concurrent indexing
+ if (_indexingState.value == IndexingState.INDEXING) {
+ AutoDevLogger.info(TAG) { "buildIndex: Already indexing, skipping" }
+ return@withContext
+ }
+
val workspace = WorkspaceManager.currentWorkspace📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| suspend fun buildIndex() = withContext(Dispatchers.Default) { | |
| val workspace = WorkspaceManager.currentWorkspace | |
| if (workspace == null) { | |
| AutoDevLogger.warn(TAG) { "buildIndex: No workspace available" } | |
| return@withContext | |
| } | |
| val rootPath = workspace.rootPath | |
| if (rootPath == null) { | |
| AutoDevLogger.warn(TAG) { "buildIndex: No root path available" } | |
| return@withContext | |
| } | |
| AutoDevLogger.info(TAG) { "buildIndex: workspace=$rootPath, currentState=${_indexingState.value}" } | |
| // Skip if already indexed for this workspace | |
| if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty()) { | |
| AutoDevLogger.info(TAG) { "buildIndex: Already indexed ${fileIndex.size} files" } | |
| _indexingState.value = IndexingState.READY | |
| return@withContext | |
| } | |
| _indexingState.value = IndexingState.INDEXING | |
| AutoDevLogger.info(TAG) { "buildIndex: Starting indexing..." } | |
| try { | |
| val files = mutableListOf<IndexedFile>() | |
| indexFilesRecursively(workspace, "", files, maxDepth = 6) | |
| fileIndex = files | |
| indexedWorkspacePath = rootPath | |
| _indexingState.value = IndexingState.READY | |
| AutoDevLogger.info(TAG) { "Index built: ${files.size} files" } | |
| } catch (e: Exception) { | |
| AutoDevLogger.error(TAG) { "Index error: ${e.message}" } | |
| e.printStackTrace() | |
| _indexingState.value = IndexingState.ERROR | |
| } | |
| } | |
| suspend fun buildIndex() = withContext(Dispatchers.Default) { | |
| // Prevent concurrent indexing | |
| if (_indexingState.value == IndexingState.INDEXING) { | |
| AutoDevLogger.info(TAG) { "buildIndex: Already indexing, skipping" } | |
| return@withContext | |
| } | |
| val workspace = WorkspaceManager.currentWorkspace | |
| if (workspace == null) { | |
| AutoDevLogger.warn(TAG) { "buildIndex: No workspace available" } | |
| return@withContext | |
| } | |
| val rootPath = workspace.rootPath | |
| if (rootPath == null) { | |
| AutoDevLogger.warn(TAG) { "buildIndex: No root path available" } | |
| return@withContext | |
| } | |
| AutoDevLogger.info(TAG) { "buildIndex: workspace=$rootPath, currentState=${_indexingState.value}" } | |
| // Skip if already indexed for this workspace | |
| if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty()) { | |
| AutoDevLogger.info(TAG) { "buildIndex: Already indexed ${fileIndex.size} files" } | |
| _indexingState.value = IndexingState.READY | |
| return@withContext | |
| } | |
| _indexingState.value = IndexingState.INDEXING | |
| AutoDevLogger.info(TAG) { "buildIndex: Starting indexing..." } | |
| try { | |
| val files = mutableListOf<IndexedFile>() | |
| indexFilesRecursively(workspace, "", files, maxDepth = 6) | |
| fileIndex = files | |
| indexedWorkspacePath = rootPath | |
| _indexingState.value = IndexingState.READY | |
| AutoDevLogger.info(TAG) { "Index built: ${files.size} files" } | |
| } catch (e: Exception) { | |
| AutoDevLogger.error(TAG) { "Index error: ${e.message}" } | |
| e.printStackTrace() | |
| _indexingState.value = IndexingState.ERROR | |
| } | |
| } |
🤖 Prompt for AI Agents
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt
around lines 47-84: buildIndex can run concurrently and multiple coroutines may
pass the pre-checks causing redundant indexing; add a concurrency guard (e.g., a
Mutex or an AtomicBoolean/Volatile state) at the start of buildIndex so only one
coroutine proceeds to index for a given workspace: acquire the guard, re-check
indexedWorkspacePath/fileIndex and current indexing state, set _indexingState to
INDEXING (or mark the atomic flag) before doing work, and release/clear the
guard in a finally block (also ensure state is set to READY or ERROR
appropriately) to prevent overlapping index runs for the same workspace.
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive file context management for mpp-ui Compose, bringing feature parity with VSCode and IntelliJ IDEA implementations. The changes introduce a file search and selection system with pre-built indexing for fast searching, along with UI components to manage file context in the DevIns editor.
- Adds indexed file search with WorkspaceFileSearchProvider for instant file lookup
- Implements TopToolbar with file chips, FileSearchPopup with grouped results, and FileChip with hover effects
- Integrates file context into DevInEditorInput with automatic DevIns command generation (/file: and /dir:)
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| AutoDevComposeIcons.kt | Adds AutoAwesome icon for prompt enhancement feature |
| WorkspaceFileSearchProvider.kt | Implements core file indexing and search logic with IndexingState management |
| TopToolbar.kt | Provides file context toolbar with add/remove controls and expandable file list |
| SelectedFileItem.kt | Data class for selected files with DevIns command conversion and path truncation |
| FileSearchPopup.kt | Dropdown search interface with recent files, indexing status, and grouped results |
| FileChip.kt | File chip components with hover-based remove button (collapsed and expanded views) |
| DevInEditorInput.kt | Integrates TopToolbar and implements buildAndSendMessage() to append file context |
| BottomToolbar.kt | Adds Prompt Enhancement button with loading state |
| DefaultFileSystem.jvm.kt | Adds debugging println statements for file search troubleshooting |
| EditorCallbacks.kt | Extends callback interface with FileContext data class and overloaded onSubmit method |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (parts.size <= 2) return "...$parentPath" | ||
|
|
||
| val keepParts = parts.takeLast(4) | ||
| return "...${keepParts.joinToString("/")}" |
There was a problem hiding this comment.
The truncatedPath property and standalone truncatePath() function have inconsistent formatting. In line 38, when the path is truncated, it returns "...${keepParts.joinToString("/")}" without a separator after "...", while the standalone function (line 74) returns ".../${keepParts.joinToString("/")}" with a "/" separator.
For consistency and better readability, both should use the same format. Consider:
return ".../\${keepParts.joinToString("/")}"This makes it clear that the ellipsis represents omitted directory levels.
| return "...${keepParts.joinToString("/")}" | |
| return ".../${keepParts.joinToString("/")}" |
| private var fileIndex: List<IndexedFile> = emptyList() | ||
| private var indexedWorkspacePath: String? = null | ||
|
|
||
| data class IndexedFile( | ||
| val name: String, | ||
| val relativePath: String, | ||
| val isDirectory: Boolean | ||
| ) | ||
|
|
||
| /** | ||
| * Build the file index for the current workspace. | ||
| * Should be called when workspace is opened. | ||
| */ | ||
| suspend fun buildIndex() = withContext(Dispatchers.Default) { | ||
| val workspace = WorkspaceManager.currentWorkspace | ||
| if (workspace == null) { | ||
| AutoDevLogger.warn(TAG) { "buildIndex: No workspace available" } | ||
| return@withContext | ||
| } | ||
|
|
||
| val rootPath = workspace.rootPath | ||
| if (rootPath == null) { | ||
| AutoDevLogger.warn(TAG) { "buildIndex: No root path available" } | ||
| return@withContext | ||
| } | ||
|
|
||
| AutoDevLogger.info(TAG) { "buildIndex: workspace=$rootPath, currentState=${_indexingState.value}" } | ||
|
|
||
| // Skip if already indexed for this workspace | ||
| if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty()) { | ||
| AutoDevLogger.info(TAG) { "buildIndex: Already indexed ${fileIndex.size} files" } | ||
| _indexingState.value = IndexingState.READY | ||
| return@withContext | ||
| } | ||
|
|
||
| _indexingState.value = IndexingState.INDEXING | ||
| AutoDevLogger.info(TAG) { "buildIndex: Starting indexing..." } | ||
|
|
||
| try { | ||
| val files = mutableListOf<IndexedFile>() | ||
| indexFilesRecursively(workspace, "", files, maxDepth = 6) | ||
| fileIndex = files | ||
| indexedWorkspacePath = rootPath | ||
| _indexingState.value = IndexingState.READY | ||
| AutoDevLogger.info(TAG) { "Index built: ${files.size} files" } | ||
| } catch (e: Exception) { | ||
| AutoDevLogger.error(TAG) { "Index error: ${e.message}" } | ||
| e.printStackTrace() | ||
| _indexingState.value = IndexingState.ERROR | ||
| } | ||
| } |
There was a problem hiding this comment.
There's a potential race condition in the index building logic. The fileIndex and indexedWorkspacePath variables are accessed without synchronization:
- Line 63: Check
if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty()) - Line 74-76: Update
fileIndexandindexedWorkspacePath - Line 138-139: Read
fileIndexinsearchFiles()
If multiple coroutines call buildIndex() or searchFiles() concurrently, they could read stale data or see inconsistent state.
Consider using a Mutex or making these properties thread-safe:
private val indexLock = Mutex()
private var fileIndex: List<IndexedFile> = emptyList()
suspend fun buildIndex() = withContext(Dispatchers.Default) {
indexLock.withLock {
// existing logic
}
}|
|
||
| isLoading = true | ||
| scope.launch { | ||
| delay(150) // Debounce |
There was a problem hiding this comment.
[nitpick] The search debounce delay of 150ms (line 104) is a magic number without explanation. Consider extracting it as a named constant for better maintainability:
companion object {
private const val SEARCH_DEBOUNCE_MS = 150L
}This makes it easier to adjust the debounce timing and documents the intentional delay.
| * 当用户提交内容时调用,包含文件上下文 | ||
| * 默认实现调用不带文件上下文的 onSubmit | ||
| */ | ||
| fun onSubmit(text: String, files: List<FileContext>) { |
There was a problem hiding this comment.
The default implementation of onSubmit(text: String, files: List<FileContext>) that delegates to onSubmit(text: String) may lead to silent data loss. If a caller implements only the single-parameter version but the system calls the two-parameter version with file contexts, those file contexts will be silently ignored.
Consider adding logging or making the interface more explicit about which method should be overridden:
/**
* 当用户提交内容时调用,包含文件上下文
* 重要:如果你的实现需要处理文件上下文,必须重写此方法
*/
fun onSubmit(text: String, files: List<FileContext>) {
if (files.isNotEmpty()) {
AutoDevLogger.warn("EditorCallbacks") {
"File contexts provided but not handled by implementation"
}
}
onSubmit(text)
}This makes it clear when file context data is being dropped.
| fun onSubmit(text: String, files: List<FileContext>) { | |
| fun onSubmit(text: String, files: List<FileContext>) { | |
| if (files.isNotEmpty()) { | |
| AutoDevLogger.warn("EditorCallbacks") { "File contexts provided but not handled by implementation" } | |
| } |
| /** | ||
| * Truncate path for display, showing last 3-4 parts. | ||
| */ | ||
| fun truncatePath(path: String, maxLength: Int = 30): String { | ||
| val parentPath = path.substringBeforeLast('/') | ||
| if (parentPath.isEmpty() || parentPath == path) return "" | ||
|
|
||
| if (parentPath.length <= maxLength) return parentPath | ||
|
|
||
| val parts = parentPath.split('/') | ||
| if (parts.size <= 2) return "...$parentPath" | ||
|
|
||
| val keepParts = parts.takeLast(3) | ||
| return ".../${keepParts.joinToString("/")}" | ||
| } | ||
|
|
There was a problem hiding this comment.
The standalone truncatePath function (lines 64-75) appears to be unused and duplicates logic similar to the truncatedPath property. Consider removing it if it's not used elsewhere, or document its intended use case if it differs from truncatedPath.
Additionally, this function has inconsistent behavior compared to truncatedPath:
- Uses
maxLengthparameter (default 30) vs hardcoded 40 - Takes last 3 parts vs last 4 parts
- Returns ".../" prefix vs "..." prefix
If both are needed, document why they differ.
| /** | |
| * Truncate path for display, showing last 3-4 parts. | |
| */ | |
| fun truncatePath(path: String, maxLength: Int = 30): String { | |
| val parentPath = path.substringBeforeLast('/') | |
| if (parentPath.isEmpty() || parentPath == path) return "" | |
| if (parentPath.length <= maxLength) return parentPath | |
| val parts = parentPath.split('/') | |
| if (parts.size <= 2) return "...$parentPath" | |
| val keepParts = parts.takeLast(3) | |
| return ".../${keepParts.joinToString("/")}" | |
| } |
| val results = fileIndex | ||
| .filter { it.name.lowercase().contains(lowerQuery) } | ||
| .take(50) | ||
| .map { indexed -> | ||
| SelectedFileItem( | ||
| name = indexed.name, | ||
| path = fileSystem.resolvePath(indexed.relativePath), | ||
| relativePath = indexed.relativePath, | ||
| isDirectory = indexed.isDirectory | ||
| ) | ||
| } | ||
| .sortedWith(compareBy({ !it.isDirectory }, { it.name.lowercase() })) |
There was a problem hiding this comment.
The search filtering on line 139 uses contains() which performs a linear scan of each filename. For large indexes (up to 5000 files), this could be slow. Additionally, calling .lowercase() on every file name during each search is inefficient.
Consider optimizing by:
- Pre-computing lowercase names during indexing:
data class IndexedFile(
val name: String,
val nameLowercase: String = name.lowercase(),
val relativePath: String,
val isDirectory: Boolean
)- For more advanced use cases, consider using a trie or other prefix-matching data structure for faster searching.
This would improve search responsiveness, especially with large file counts.
| class InMemoryRecentFilesProvider(private val maxSize: Int = 20) : RecentFilesProvider { | ||
| private val recentFiles = mutableListOf<SelectedFileItem>() | ||
|
|
||
| override suspend fun getRecentFiles(): List<SelectedFileItem> = recentFiles.toList() | ||
|
|
||
| override fun addRecentFile(file: SelectedFileItem) { | ||
| recentFiles.removeAll { it.path == file.path } | ||
| recentFiles.add(0, file) | ||
| if (recentFiles.size > maxSize) recentFiles.removeAt(recentFiles.lastIndex) | ||
| } |
There was a problem hiding this comment.
The InMemoryRecentFilesProvider class is not thread-safe. The recentFiles mutableList is accessed by multiple methods without synchronization:
- Line 194:
getRecentFiles()creates a copy withtoList() - Lines 197-199:
addRecentFile()modifies the list
If addRecentFile() is called concurrently with getRecentFiles() from different threads/coroutines, it could lead to ConcurrentModificationException or inconsistent state.
Consider using thread-safe alternatives:
private val recentFiles = mutableListOf<SelectedFileItem>()
private val lock = Any()
override suspend fun getRecentFiles(): List<SelectedFileItem> = synchronized(lock) {
recentFiles.toList()
}
override fun addRecentFile(file: SelectedFileItem) = synchronized(lock) {
recentFiles.removeAll { it.path == file.path }
recentFiles.add(0, file)
if (recentFiles.size > maxSize) recentFiles.removeAt(recentFiles.lastIndex)
}| if (parentPath.length <= 40) return parentPath | ||
|
|
||
| val parts = parentPath.split("/") | ||
| if (parts.size <= 2) return "...$parentPath" |
There was a problem hiding this comment.
The truncatedPath logic may produce inconsistent results when parts.size <= 2. For a path like "a/b", it would return "...a/b" which looks odd with the ellipsis. Consider checking if truncation is actually needed:
val parts = parentPath.split("/")
if (parts.size <= 2) return parentPath // No need for ellipsisThis ensures that short paths display cleanly without unnecessary ellipsis.
| if (currentDepth >= maxDepth || files.size >= 5000) return | ||
|
|
||
| val fileSystem = workspace.fileSystem | ||
| val pathToList = if (currentPath.isEmpty()) "." else currentPath | ||
|
|
||
| try { | ||
| val entries = fileSystem.listFiles(pathToList, null) | ||
| if (currentDepth == 0) { | ||
| AutoDevLogger.info(TAG) { "indexFilesRecursively: root entries=${entries.size}" } | ||
| } | ||
| for (entry in entries) { | ||
| val name = entry.substringAfterLast('/') | ||
|
|
||
| // Skip hidden files and common ignored directories | ||
| if (name.startsWith(".") || name in IGNORED_DIRS) continue | ||
|
|
||
| val isDir = fileSystem.isDirectory(fileSystem.resolvePath(entry)) | ||
| files.add(IndexedFile(name, entry, isDir)) | ||
|
|
||
| if (isDir && files.size < 5000) { | ||
| indexFilesRecursively(workspace, entry, files, maxDepth, currentDepth + 1) | ||
| } |
There was a problem hiding this comment.
The buildIndex() function limits indexing to 5000 files (line 93), but this limit is checked in two places with slightly different logic:
- Line 93:
currentDepth >= maxDepth || files.size >= 5000 - Line 112:
if (isDir && files.size < 5000)
This could lead to unexpected behavior where the function stops at exactly 5000 files but might still try to recurse. Consider using a consistent check:
if (currentDepth >= maxDepth || files.size >= MAX_INDEX_SIZE) return
// ... later ...
if (isDir && files.size < MAX_INDEX_SIZE) {
indexFilesRecursively(...)
}Where MAX_INDEX_SIZE is a named constant defined at the companion object level.
| if (query.length < 2 || currentWorkspace == null || indexingState != IndexingState.READY) { | ||
| searchResults = emptyList() | ||
| isLoading = false | ||
| return | ||
| } | ||
|
|
||
| isLoading = true | ||
| scope.launch { | ||
| delay(150) // Debounce | ||
| try { | ||
| searchResults = searchProvider.searchFiles(query) | ||
| } catch (e: Exception) { | ||
| searchResults = emptyList() | ||
| } finally { | ||
| isLoading = false | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Filter out already selected files | ||
| val displayItems = remember(searchQuery, searchResults, recentFiles, selectedFiles) { | ||
| val items = if (searchQuery.length >= 2) searchResults else recentFiles | ||
| items.filter { item -> selectedFiles.none { it.path == item.path } } | ||
| } | ||
|
|
||
| // Separate files and folders | ||
| val files = displayItems.filter { !it.isDirectory } | ||
| val folders = displayItems.filter { it.isDirectory } | ||
|
|
||
| DropdownMenu( | ||
| expanded = expanded, | ||
| onDismissRequest = onDismiss, | ||
| modifier = modifier.widthIn(min = 300.dp, max = 400.dp), | ||
| offset = DpOffset(0.dp, 4.dp) | ||
| ) { | ||
| // Search field at top | ||
| SearchField( | ||
| value = searchQuery, | ||
| onValueChange = { | ||
| searchQuery = it | ||
| performSearch(it) | ||
| }, | ||
| focusRequester = focusRequester, | ||
| onDismiss = onDismiss | ||
| ) | ||
|
|
||
| HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) | ||
|
|
||
| // Content based on state | ||
| when { | ||
| currentWorkspace == null -> { | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth().padding(16.dp), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Text( | ||
| text = "No workspace opened", | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) | ||
| ) | ||
| } | ||
| } | ||
| indexingState == IndexingState.INDEXING -> { | ||
| Row( | ||
| modifier = Modifier.fillMaxWidth().padding(16.dp), | ||
| horizontalArrangement = Arrangement.Center, | ||
| verticalAlignment = Alignment.CenterVertically | ||
| ) { | ||
| CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp) | ||
| Spacer(modifier = Modifier.width(8.dp)) | ||
| Text( | ||
| text = "Indexing files...", | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) | ||
| ) | ||
| } | ||
| } | ||
| indexingState == IndexingState.ERROR -> { | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth().padding(16.dp), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Text( | ||
| text = "Failed to index files", | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f) | ||
| ) | ||
| } | ||
| } | ||
| isLoading -> { | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth().padding(16.dp), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp) | ||
| } | ||
| } | ||
| displayItems.isEmpty() -> { | ||
| Box( | ||
| modifier = Modifier.fillMaxWidth().padding(16.dp), | ||
| contentAlignment = Alignment.Center | ||
| ) { | ||
| Text( | ||
| text = if (searchQuery.length >= 2) "No files found" else "Type to search...", | ||
| style = MaterialTheme.typography.bodySmall, | ||
| color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) | ||
| ) | ||
| } | ||
| } | ||
| searchQuery.length < 2 -> { |
There was a problem hiding this comment.
[nitpick] The search requires a minimum query length of 2 characters (lines 96, 205), but this threshold is hardcoded in multiple places. Consider extracting this as a named constant in the companion object:
companion object {
private const val MIN_QUERY_LENGTH = 2
// ...
}This improves maintainability and makes it easier to adjust the threshold if needed.
Summary
Implement file context management features for mpp-ui Compose, migrating VSCode DevInInput features for feature parity across all platforms.
Changes
New Components
SelectedFileItem.kt- Data class for selected files withtoDevInsCommand()andtruncatedPathFileChip.kt- File chip component with hover-to-show remove buttonTopToolbar.kt- Top toolbar with file context management and "Add context" buttonFileSearchPopup.kt- Dropdown menu for searching and selecting filesWorkspaceFileSearchProvider.kt- File search provider with pre-built index for fast searchEnhanced Components
DevInEditorInput.kt- Integrated TopToolbar, FileSearchPopup andbuildAndSendMessage()functionBottomToolbar.kt- Added Prompt Enhancement button (AutoAwesome icon)AutoDevComposeIcons.kt- Added AutoAwesome and History iconsEditorCallbacks.kt- AddedFileContextdata class for file context submissionKey Features
/file:pathor/dir:pathcommandsArchitecture
Closes #35
Pull Request opened by Augment Code with guidance from the PR author
Summary by CodeRabbit
New Features
Bug Fixes
Chores
✏️ Tip: You can customize this high-level summary in your review settings.