Skip to content

Comments

feat(mpp-ui): implement file context management with indexed search#36

Merged
phodal merged 1 commit intomasterfrom
feature/mpp-vscode
Dec 3, 2025
Merged

feat(mpp-ui): implement file context management with indexed search#36
phodal merged 1 commit intomasterfrom
feature/mpp-vscode

Conversation

@phodal
Copy link
Member

@phodal phodal commented Dec 3, 2025

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 with toDevInsCommand() and truncatedPath
  • FileChip.kt - File chip component with hover-to-show remove button
  • TopToolbar.kt - Top toolbar with file context management and "Add context" button
  • FileSearchPopup.kt - Dropdown menu for searching and selecting files
  • WorkspaceFileSearchProvider.kt - File search provider with pre-built index for fast search

Enhanced Components

  • DevInEditorInput.kt - Integrated TopToolbar, FileSearchPopup and buildAndSendMessage() function
  • BottomToolbar.kt - Added Prompt Enhancement button (AutoAwesome icon)
  • AutoDevComposeIcons.kt - Added AutoAwesome and History icons
  • EditorCallbacks.kt - Added FileContext data class for file context submission

Key Features

  • Indexed File Search: Pre-builds file index when popup opens for instant search
  • Indexing State: Shows "Indexing files..." status during index building
  • Grouped Results: Displays Recent Files, Files, and Folders in separate sections
  • DevIns Commands: Converts selected files to /file:path or /dir:path commands
  • Hover Effects: Remove button only visible on hover (matching IDEA version)

Architecture

FileSearchProvider (interface)
    ├── DefaultFileSearchProvider (empty implementation)
    └── WorkspaceFileSearchProvider (uses ProjectFileSystem)
            ├── IndexedFile (in-memory file index)
            ├── IndexingState (NOT_STARTED, INDEXING, READY, ERROR)
            └── RecentFilesProvider (optional)
                    └── InMemoryRecentFilesProvider

Closes #35


Pull Request opened by Augment Code with guidance from the PR author

Summary by CodeRabbit

  • New Features

    • Added file context support to message submissions
    • Introduced prompt enhancement button in editor toolbar
    • Added file search and selection interface with workspace indexing
    • New file management toolbar for adding, removing, and organizing selected files
    • Visual file chips for displaying selected files with hover actions
  • Bug Fixes

    • Enhanced error handling in file system operations
  • Chores

    • Added debugging instrumentation to file system operations

✏️ Tip: You can customize this high-level summary in your review settings.

- 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
Copilot AI review requested due to automatic review settings December 3, 2025 08:23
@coderabbitai
Copy link

coderabbitai bot commented Dec 3, 2025

Walkthrough

The 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

Cohort / File(s) Summary
File Context Foundation
mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt
Added FileContext data class with name, path, relativePath, and isDirectory fields. Introduced onSubmit(text, files) method overload to EditorCallbacks interface; existing onSubmit(text) preserved.
Debug & Instrumentation
mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt
Added comprehensive debug logging to searchFiles: call entry, regex pattern, file-walk timing, result count, and exception details. No control-flow changes; logging and timing instrumentation only.
Data Models
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt
New data class representing selected files/directories with toDevInsCommand() method for DevIns command generation, truncatedPath computed property for display, and fromPath() companion constructor. Includes truncatePath() utility function for ellipsized path display.
File UI Components
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt
Added FileChip (compact hover-responsive chip) and FileChipExpanded (full horizontal list representation) composables for displaying selected files with remove buttons and optional path truncation. Both support theme-driven styling and hover state handling.
File Search UI
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
New composable for file selection with pluggable search providers, debounced search flow, loading/indexing state handling, recent-files support, and sections for recent files, files, and folders. Defined FileSearchProvider interface and DefaultFileSearchProvider stub implementation. Includes internal helpers (SearchField, SectionHeader, FileMenuItem, MoreItemsHint).
File Context Toolbar
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt
New public composable rendering file management toolbar with add-file action, selected file chips (limited with "+N more" indicator), auto-add-current-file toggle, clear-all option, and expandable full-file-list view. Includes animated expansion and responsive layout.
Workspace File Search
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/WorkspaceFileSearchProvider.kt
New WorkspaceFileSearchProvider implementing FileSearchProvider with in-memory indexing, recursive workspace scanning (max depth 6, max 5000 files), debounced search, and recent-files management. Defined IndexingState enum (NOT_STARTED, INDEXING, READY, ERROR) and RecentFilesProvider interface with InMemoryRecentFilesProvider implementation.
Editor Integration
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt
Added fileSearchProvider parameter, file selection state (selectedFiles, autoAddCurrentFile), and buildAndSendMessage() to assemble messages with file contexts. Integrated TopToolbar (desktop only) for file management and FileSearchPopup for file selection. Replaced direct onSubmit calls with buildAndSendMessage across all submit paths. Added onEnhanceClick support in BottomToolbar wiring.
Bottom Toolbar Enhancement
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt
Added onEnhanceClick and isEnhancing parameters to toolbar signature. Introduced new Prompt Enhancement button with AutoAwesome icon; button shows CircularProgressIndicator and is disabled when isEnhancing=true, otherwise enabled with icon.
Icon Library
mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.kt
Added public AutoAwesome property exposing Icons.Default.AutoAwesome as ImageVector getter.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • WorkspaceFileSearchProvider: Recursive indexing logic with depth/size limits, state management (IndexingState), and interaction with workspace filesystem. Verify index rebuild triggers, performance under large workspaces, and error recovery.
  • DevInEditorInput integration: Significant refactoring with message composition logic via buildAndSendMessage(), state synchronization between TopToolbar/FileSearchPopup, and consistency across multiple submit paths (text send, Enter, command-triggered). Ensure file contexts are consistently included without regressions on mobile.
  • TopToolbar & FileChip: Hover state handling via MutableInteractionSource, animated expansion/collapse, and adaptive responsive layout. Test interaction flows and layout edge cases (empty state, many files).
  • FileSearchPopup: Debounced search flow, provider abstraction, and recent-files fallback logic. Verify search/indexing state coordination and UI state transitions (loading → ready → results).

Possibly related PRs

  • phodal/auto-dev-sketch#28: Implements file-search and file-selection UIs with integration into editor submit flow, directly parallel to this PR's multiplatform Compose approach.
  • phodal/auto-dev-sketch#23: Modifies editor input and toolbar with prompt-enhancement UI and AutoAwesome icon, overlapping feature set with this PR's bottom-toolbar enhancements.
  • phodal/auto-dev-sketch#30: Implements closely related file-search and file-context UI components (SelectedFileItem, file search popups, toolbars) with similar architecture to this PR's approach.

Poem

🐰 Files now hop into the editor's fold,
With chips and toolbars, stories yet untold.
Search through workspaces, indexed with care,
Context flows onward, floating through air!
✨ DevIns now dances with files by its side.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.83% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main feature: file context management with indexed search capability added to mpp-ui.
Linked Issues check ✅ Passed All four phases of issue #35 are fully implemented: file context management, bottom toolbar enhancement, FileSearchPopup, and platform integration with WorkspaceFileSearchProvider.
Out of Scope Changes check ✅ Passed Debug logging added to DefaultFileSystem.jvm.kt is a minor supporting change; all other modifications directly support the stated objectives.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/mpp-vscode

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@augmentcode augmentcode bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 3 suggestions posted.

Comment augment review to trigger a new review at any time.

private val _indexingState = MutableStateFlow(IndexingState.NOT_STARTED)
val indexingState: StateFlow<IndexingState> = _indexingState

private var fileIndex: List<IndexedFile> = emptyList()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 👎

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt (3)

108-113: Console println debug logging should be temporary or behind a logger/config flag

The added println calls and e.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 centralizing

The 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 or PatternSyntaxExceptions.
  • 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‑coded build exclusion and per‑call gitignore reload may not always be desirable

Two behavioral points to double‑check:

  • criticalExcludeDirs = setOf(".git", "build") now permanently excludes any files under a build directory, regardless of .gitignore. This matches your test expectations but may surprise users in repos where build contains meaningful sources. Consider making this configurable or relying on .gitignore for non‑critical dirs.
  • gitIgnoreParser?.reload() on every searchFiles call 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 for relativePath.

The default relativePath = name seems semantically incorrect. A relative path should typically default to path or be derived from it, not from name (which is just the filename). If the caller doesn't provide relativePath, 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 isExpanded is true and selectedFiles.isNotEmpty(), neither the file chips row (line 96) nor the spacer (line 124) is rendered in the main toolbar row. This leaves no weight(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 with truncatePath function.

The truncatedPath property (lines 28-39) and the standalone truncatePath function (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 in InMemoryRecentFilesProvider.

The recentFiles list uses a non-thread-safe mutableListOf. If addRecentFile and getRecentFiles are called from different threads, this could cause a ConcurrentModificationException.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 40c4593 and 29e1e18.

📒 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: Use expect/actual for 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: Avoid Flow, use Promise instead
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.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt
  • mpp-core/src/jvmMain/kotlin/cc/unitmesh/devins/filesystem/DefaultFileSystem.jvm.kt
  • mpp-core/src/commonMain/kotlin/cc/unitmesh/devins/editor/EditorCallbacks.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
  • mpp-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 AutoDevColors from cc.unitmesh.devins.ui.compose.theme or MaterialTheme.colorScheme

Files:

  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/icons/AutoDevComposeIcons.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
  • mpp-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.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileChip.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/SelectedFileItem.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/TopToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/DevInEditorInput.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/BottomToolbar.kt
  • mpp-ui/src/commonMain/kotlin/cc/unitmesh/devins/ui/compose/editor/context/FileSearchPopup.kt
  • mpp-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.kt
  • mpp-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 onSubmit method with file context properly defaults to the existing onSubmit(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 AutoAwesome icon 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 CircularProgressIndicator when enhancing, and properly disables the button during the operation. All colors are sourced from MaterialTheme.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 ContextIndicator component 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.isIOS checks 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 FileChip component properly implements hover-based interactions matching IDEA behavior. All colors are derived from MaterialTheme.colorScheme, complying with the coding guidelines. The combination of clip, hoverable, and border modifiers is correctly ordered for proper visual effects.


110-173: LGTM!

The FileChipExpanded component correctly provides a fuller representation with the remove button always visible (with hover-based tint adjustment). The layout with weight(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 fromPath factory 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 remember block correctly memoizes the effective provider, and the fallback to WorkspaceFileSearchProvider provides sensible defaults.


121-139: LGTM!

The buildAndSendMessage function 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 enhanceCurrentInput function 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 FileSearchProvider interface provides a clean abstraction for platform-specific implementations, and DefaultFileSearchProvider offers a sensible no-op fallback.


371-377: Color usage complies with guidelines.

All colors correctly use MaterialTheme.colorScheme tokens rather than hardcoded values. Based on coding guidelines.


262-293: LGTM!

The SearchField component properly handles keyboard events and uses theme-compliant styling.


323-378: LGTM!

The FileMenuItem component 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 IndexingState enum 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_DIRS set covers common build artifacts and IDE directories appropriately.

Comment on lines +94 to +113
// 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
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +64 to +75
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("/")}"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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.

Comment on lines +47 to +84
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
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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("/")}"
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
return "...${keepParts.joinToString("/")}"
return ".../${keepParts.joinToString("/")}"

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +84
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
}
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition in the index building logic. The fileIndex and indexedWorkspacePath variables are accessed without synchronization:

  1. Line 63: Check if (indexedWorkspacePath == rootPath && fileIndex.isNotEmpty())
  2. Line 74-76: Update fileIndex and indexedWorkspacePath
  3. Line 138-139: Read fileIndex in searchFiles()

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
    }
}

Copilot uses AI. Check for mistakes.

isLoading = true
scope.launch {
delay(150) // Debounce
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot uses AI. Check for mistakes.
* 当用户提交内容时调用,包含文件上下文
* 默认实现调用不带文件上下文的 onSubmit
*/
fun onSubmit(text: String, files: List<FileContext>) {
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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" }
}

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +76
/**
* 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("/")}"
}

Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 maxLength parameter (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.

Suggested change
/**
* 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("/")}"
}

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +149
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() }))
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Pre-computing lowercase names during indexing:
data class IndexedFile(
    val name: String,
    val nameLowercase: String = name.lowercase(),
    val relativePath: String,
    val isDirectory: Boolean
)
  1. 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.

Copilot uses AI. Check for mistakes.
Comment on lines +191 to +200
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)
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The InMemoryRecentFilesProvider class is not thread-safe. The recentFiles mutableList is accessed by multiple methods without synchronization:

  • Line 194: getRecentFiles() creates a copy with toList()
  • 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)
}

Copilot uses AI. Check for mistakes.
if (parentPath.length <= 40) return parentPath

val parts = parentPath.split("/")
if (parts.size <= 2) return "...$parentPath"
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ellipsis

This ensures that short paths display cleanly without unnecessary ellipsis.

Copilot uses AI. Check for mistakes.
Comment on lines +93 to +114
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)
}
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +205
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 -> {
Copy link

Copilot AI Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copilot uses AI. Check for mistakes.
@phodal phodal merged commit 88781b5 into master Dec 3, 2025
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Compose] Migrate VSCode DevInInput features to mpp-ui Compose

1 participant