Skip to content

ADFA-4419: Remote peer editor decoration API#1459

Open
Daniel-ADFA wants to merge 8 commits into
stagefrom
feat/ADFA-4419-remote-peer-editor-decoration
Open

ADFA-4419: Remote peer editor decoration API#1459
Daniel-ADFA wants to merge 8 commits into
stagefrom
feat/ADFA-4419-remote-peer-editor-decoration

Conversation

@Daniel-ADFA

@Daniel-ADFA Daniel-ADFA commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Host-side support for the pair-programming plugin (ADFA-4419): lets the external .cgp pair plugin render remote collaborators in the editor and drive shared editing entirely through plugin-api. The pair plugin itself ships separately; this PR adds only the host contract + bridge it talks to.

Daniel-ADFA and others added 8 commits May 28, 2026 23:42
Add IdeEditorService.addRemotePeerMarker/removeRemotePeerMarker/clearRemotePeerMarkers as default-implemented (backward-compatible) methods so a plugin can draw a remote collaborator's caret/badge inside the editor.

Backed by a new EditorDecorationManager + RemotePeerMarkerWindow (an EditorPopupWindow overlay that tracks scroll via FEATURE_SCROLL_AS_CONTENT) in the app module; EditorProviderImpl resolves the live editor via EditorHandlerActivity.getEditorForFile and marshals onto the main thread, clearing markers on dispose. IdeEditorServiceImpl exposes read-gated overrides and the parallel EditorProvider contract methods.

The new interface methods are default-implemented so this is an additive, non-breaking change for the generated plugin-api lib and existing implementers. Consumed by the Pair pair-programming plugin.
…orations

Rename EditorDecorationManager -> PeerPresenceOverlayManager and extract a
focused PeerPresenceProvider interface out of the broad EditorProvider, so the
pair-programming peer-cursor overlay (floating named badges) reads as a distinct
concern from the generic EditorDecorationProvider (additive color spans) added
in #1448 (ADFA-4436).

Host-internal only: no plugin-api contract changed and the merged rainbow-
brackets plugin is unaffected. Verified with :app:compileV8DebugKotlin.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@Daniel-ADFA Daniel-ADFA changed the title ADFA-4419: Remote peer editor decoration (pair programming) ADFA-4419: Remote peer editor decoration API Jun 28, 2026
@Daniel-ADFA

Daniel-ADFA commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

@claude review

@Daniel-ADFA Daniel-ADFA requested a review from a team June 28, 2026 23:52
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough
  • Added host-side support for remote peer presence in the editor, including floating caret/name badges that update per file and per collaborator.
  • Extended plugin-facing services with new default-bodied APIs for showing/hiding/clearing peer cursors and opening a project from a synced directory, preserving binary compatibility.
  • Wired the new peer-cursor flow through the host implementation stack (EditorProviderImpl, IdeEditorServiceImpl, PluginManager) so remote cursor updates are validated, permission-checked, and rendered by the editor overlay manager.
  • Added project-open handling in IdeProjectServiceImpl, including permission checks, path restrictions to the projects directory, activity recreation, and state updates for the last opened project.
  • Improved plugin installation feedback by verifying that a plugin actually loads after installation and surfacing recorded load errors when it does not.
  • Risk: the new overlay-based peer cursor rendering is a separate flow from EditorDecorationProvider, so there is potential for duplicate decoration concepts if both are enabled or misunderstood.
  • Risk: openProject(projectDir) performs privileged file-system and activity operations; incorrect permission or path validation could expose security or stability issues.
  • Risk: peer cursor overlays depend on editor instance lookup and UI-thread lifecycle handling, so stale editor references or timing issues may cause incorrect marker placement or cleanup gaps.

Walkthrough

Adds a peer-cursor overlay system that renders per-peer editor badges via PeerPresenceOverlayManager and PeerCursorWindow. Extends the plugin service API with showPeerCursor and openProject contracts, implements openProject with security/path enforcement in IdeProjectServiceImpl, and introduces plugin load-failure tracking in PluginManager with install-time verification in PluginRepositoryImpl.

Changes

Peer Cursor Overlay Feature

Layer / File(s) Summary
Plugin API contracts
plugin-api/.../IdeServices.kt
Adds default openProject(projectDir) to IdeProjectService and showPeerCursor(...) to IdeEditorService with KDoc describing semantics, permissions, and repositioning behavior.
EditorService peer-cursor delegation
plugin-manager/.../IdeEditorServiceImpl.kt, plugin-manager/.../PluginManager.kt
Introduces PeerPresenceProvider interface, extends EditorProvider with it, adds showPeerCursor/hidePeerCursor/clearPeerCursors overrides enforcing read permissions, and wires delegation through PluginManager.
PeerPresenceOverlayManager and PeerCursorWindow
app/.../PeerPresenceOverlayManager.kt
Implements overlay manager with a (filePath→peerId→PeerCursorWindow) registry and marker lifecycle, and PeerCursorWindow as an EditorPopupWindow subclass rendering a positioned badge with peer name, direction arrow, peer color, and luminance-based text contrast.
EditorProviderImpl wiring and dispose
app/.../EditorProviderImpl.kt
Adds peerPresenceOverlay field, implements showPeerCursor/hidePeerCursor/clearPeerCursors delegating to overlay manager, and extends dispose() to clearAll() on the main thread.

openProject Plugin API Implementation

Layer / File(s) Summary
IdeProjectServiceImpl openProject
plugin-manager/.../IdeProjectServiceImpl.kt, plugin-manager/.../PluginManager.kt
Adds activityProvider constructor param, implements openProject with FILESYSTEM_READ enforcement, canonical-path containment under PROJECTS_DIR, directory validation, foreground-activity check, ProjectManagerImpl/GeneralPreferences update, and UI-thread activity recreation. Threads activityProvider through both createPluginContext call sites.

Plugin Load Error Tracking

Layer / File(s) Summary
PluginManager load failure recording
plugin-manager/.../PluginManager.kt
Introduces loadFailures ConcurrentHashMap, clears it at load start, wraps loadPlugin in Result to record failures via recordLoadFailure, and exposes getLoadError(pluginId) as a public API.
PluginRepositoryImpl install verification
app/.../PluginRepositoryImpl.kt
After loadPlugins(), checks manager.getPlugin(pluginId) and throws IllegalStateException with getLoadError message when the plugin is still absent.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • jatezzz
  • itsaky-adfa
  • dara-abijo-adfa

Poem

🐇 Hop! A badge floats by the caret's glow,
Each peer's name shining in a colored bow.
Security checks guard the project gate,
Load errors tracked so installs don't frustrate.
With luminance math for readable text,
The rabbit ships presence — what feature is next? ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: remote peer editor decoration support.
Description check ✅ Passed The description is directly related to the changeset and matches the peer-cursor and plugin API work.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ADFA-4419-remote-peer-editor-decoration

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/activities/editor/PeerPresenceOverlayManager.kt`:
- Around line 122-125: The clamping logic in PeerPresenceOverlayManager’s
position calculation does not handle the exact-fit case because when maxX is 0
it falls back to rawX, allowing the overlay to render off-screen; update the x
computation so the clamp applies whenever the editor width is known, including
when label.measuredWidth equals boundEditor.width, and keep the existing
rawX.coerceIn(0, maxX) behavior for all non-negative maxX values.

In
`@app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt`:
- Around line 147-154: The failure path in installPluginFromFile() leaves a
broken upgraded plugin on disk after the old version has already been removed,
so adjust the replace flow to avoid half-installed upgrades. Use the existing
load check around manager.loadPlugins()/manager.getPlugin(pluginId) to either
delay uninstalling the current plugin until the new package is proven loadable,
or restore the previous package on failure. In the failure branch that throws
IllegalStateException, also delete the copied finalFile so future
PluginRepositoryImpl loads do not keep retrying the bad artifact.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 279-284: The load path in PluginManager.loadPlugins is swallowing
coroutine cancellation by catching all Exception around loadPlugin, so a
cancelled job is treated like a plugin failure instead of stopping execution.
Narrow the catch in the loadPlugin/result block to let CancellationException
pass through unchanged, and only wrap genuine plugin load errors in
Result.failure before calling recordLoadFailure.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeEditorServiceImpl.kt`:
- Around line 283-292: `hidePeerCursor` and `clearPeerCursors` in
`IdeEditorServiceImpl` are unnecessarily gated by `ensureFileAccessible(file)`,
which blocks overlay cleanup when the file/tab is no longer open. Remove the
accessibility check from these cleanup-only paths while keeping `requireRead()`
and the downstream `editorProvider.hidePeerCursor(...)` /
`editorProvider.clearPeerCursors(...)` calls so peer overlays can still be
dismissed by file key.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeProjectServiceImpl.kt`:
- Around line 97-105: Use the validated canonical project path in
IdeProjectServiceImpl.openProject and apply the existing PathValidator before
switching projects. The current flow checks containment with
isUnderProjectsDir(projectDir) but later persists the original path, so update
the openProject logic to reuse the validated canonical target and run it through
PathValidator like getProjectByPath does before calling the project
switch/selection code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e0348b09-b91a-4370-9cb1-9c72e245fff6

📥 Commits

Reviewing files that changed from the base of the PR and between c14733b and ea2293a.

📒 Files selected for processing (7)
  • app/src/main/java/com/itsaky/androidide/activities/editor/PeerPresenceOverlayManager.kt
  • app/src/main/java/com/itsaky/androidide/app/EditorProviderImpl.kt
  • app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeServices.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeEditorServiceImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeProjectServiceImpl.kt

Comment on lines +122 to +125
// Clamp into the visible width so a caret scrolled off to the right pins at the edge
// instead of vanishing. Only clamp when the editor has a known width.
val maxX = boundEditor.width - width
val x = if (maxX > 0) rawX.coerceIn(0, maxX) else rawX

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clamp the exact-fit case too.

When label.measuredWidth == boundEditor.width, maxX becomes 0 and this branch falls back to rawX, so a long peer name can still render off-screen even though the code intends to pin it to the visible edge.

Suggested fix
-        val x = if (maxX > 0) rawX.coerceIn(0, maxX) else rawX
+        val x = if (maxX >= 0) rawX.coerceIn(0, maxX) else rawX
📝 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
// Clamp into the visible width so a caret scrolled off to the right pins at the edge
// instead of vanishing. Only clamp when the editor has a known width.
val maxX = boundEditor.width - width
val x = if (maxX > 0) rawX.coerceIn(0, maxX) else rawX
// Clamp into the visible width so a caret scrolled off to the right pins at the edge
// instead of vanishing. Only clamp when the editor has a known width.
val maxX = boundEditor.width - width
val x = if (maxX >= 0) rawX.coerceIn(0, maxX) else rawX
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/itsaky/androidide/activities/editor/PeerPresenceOverlayManager.kt`
around lines 122 - 125, The clamping logic in PeerPresenceOverlayManager’s
position calculation does not handle the exact-fit case because when maxX is 0
it falls back to rawX, allowing the overlay to render off-screen; update the x
computation so the clamp applies whenever the editor width is known, including
when label.measuredWidth equals boundEditor.width, and keep the existing
rawX.coerceIn(0, maxX) behavior for all non-negative maxX values.

Comment on lines 147 to +154
manager.loadPlugins()

if (manager.getPlugin(pluginId) == null) {
throw IllegalStateException(
manager.getLoadError(pluginId)
?: "Plugin \"$pluginId\" was installed but failed to load."
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Don't leave a failed upgrade half-installed.

This check runs after Line 123 has already removed the previous plugin and Lines 136-141 have already copied the new package into pluginsDir. If the new plugin fails to load, installPluginFromFile() now returns failure but still leaves the broken artifact on disk, so the working version is gone and future loadPlugins() calls will keep retrying the same bad package. Please either defer uninstall until the replacement has been proven loadable, or snapshot/restore the previous package and delete finalFile on this failure path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/main/java/com/itsaky/androidide/repositories/PluginRepositoryImpl.kt`
around lines 147 - 154, The failure path in installPluginFromFile() leaves a
broken upgraded plugin on disk after the old version has already been removed,
so adjust the replace flow to avoid half-installed upgrades. Use the existing
load check around manager.loadPlugins()/manager.getPlugin(pluginId) to either
delay uninstalling the current plugin until the new package is proven loadable,
or restore the previous package on failure. In the failure branch that throws
IllegalStateException, also delete the copied finalFile so future
PluginRepositoryImpl loads do not keep retrying the bad artifact.

Comment on lines +279 to +284
val result = try {
loadPlugin(pluginFile)
} catch (e: Exception) {
logger.error("Failed to load plugin from ${pluginFile.name}", e)
Result.failure(e)
}
result.onFailure { error -> recordLoadFailure(pluginFile, error) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Don't swallow coroutine cancellation here.

CancellationException is an Exception, so this turns a cancelled loadPlugins() job into a recorded plugin failure and lets awaitAll() keep running. Re-throw cancellation and only wrap real load errors.

Based on learnings, prefer narrow exception handling in Kotlin so cancellation still fails fast.

Suggested fix
                 val result = try {
                     loadPlugin(pluginFile)
+                } catch (e: kotlinx.coroutines.CancellationException) {
+                    throw e
                 } catch (e: Exception) {
                     Result.failure(e)
                 }
                 result.onFailure { error -> recordLoadFailure(pluginFile, error) }
📝 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
val result = try {
loadPlugin(pluginFile)
} catch (e: Exception) {
logger.error("Failed to load plugin from ${pluginFile.name}", e)
Result.failure(e)
}
result.onFailure { error -> recordLoadFailure(pluginFile, error) }
val result = try {
loadPlugin(pluginFile)
} catch (e: kotlinx.coroutines.CancellationException) {
throw e
} catch (e: Exception) {
Result.failure(e)
}
result.onFailure { error -> recordLoadFailure(pluginFile, error) }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`
around lines 279 - 284, The load path in PluginManager.loadPlugins is swallowing
coroutine cancellation by catching all Exception around loadPlugin, so a
cancelled job is treated like a plugin failure instead of stopping execution.
Narrow the catch in the loadPlugin/result block to let CancellationException
pass through unchanged, and only wrap genuine plugin load errors in
Result.failure before calling recordLoadFailure.

Source: Learnings

Comment on lines +283 to +292
override fun hidePeerCursor(file: File, peerId: String): Boolean {
requireRead()
ensureFileAccessible(file)
return editorProvider.hidePeerCursor(file, peerId)
}

override fun clearPeerCursors(file: File) {
requireRead()
ensureFileAccessible(file)
editorProvider.clearPeerCursors(file)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't block overlay cleanup on current file accessibility.

hidePeerCursor and clearPeerCursors only remove overlay state downstream, but these guards make normal cleanup fail once the tab is closed or the file is no longer considered accessible. That can leave stale peer badges behind even though the provider could still dismiss them by key.

Suggested fix
 override fun hidePeerCursor(file: File, peerId: String): Boolean {
     requireRead()
-    ensureFileAccessible(file)
     return editorProvider.hidePeerCursor(file, peerId)
 }
 
 override fun clearPeerCursors(file: File) {
     requireRead()
-    ensureFileAccessible(file)
     editorProvider.clearPeerCursors(file)
 }
📝 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
override fun hidePeerCursor(file: File, peerId: String): Boolean {
requireRead()
ensureFileAccessible(file)
return editorProvider.hidePeerCursor(file, peerId)
}
override fun clearPeerCursors(file: File) {
requireRead()
ensureFileAccessible(file)
editorProvider.clearPeerCursors(file)
override fun hidePeerCursor(file: File, peerId: String): Boolean {
requireRead()
return editorProvider.hidePeerCursor(file, peerId)
}
override fun clearPeerCursors(file: File) {
requireRead()
editorProvider.clearPeerCursors(file)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeEditorServiceImpl.kt`
around lines 283 - 292, `hidePeerCursor` and `clearPeerCursors` in
`IdeEditorServiceImpl` are unnecessarily gated by `ensureFileAccessible(file)`,
which blocks overlay cleanup when the file/tab is no longer open. Remove the
accessibility check from these cleanup-only paths while keeping `requireRead()`
and the downstream `editorProvider.hidePeerCursor(...)` /
`editorProvider.clearPeerCursors(...)` calls so peer overlays can still be
dismissed by file key.

Comment on lines +97 to +105
if (!isUnderProjectsDir(projectDir)) {
Log.w(TAG, "[HOST] openProject denied: ${projectDir.absolutePath} is not under projects dir ${Environment.PROJECTS_DIR?.absolutePath}")
throw SecurityException("Plugin $pluginId may only open projects under ${Environment.PROJECTS_DIR?.absolutePath}")
}

if (!projectDir.exists() || !projectDir.isDirectory) {
Log.w(TAG, "[HOST] openProject aborted: not a directory (exists=${projectDir.exists()}, isDir=${projectDir.isDirectory})")
return false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Use the validated canonical path and apply PathValidator before switching projects.

isUnderProjectsDir() validates a canonical target, but Line 114 and Line 115 persist the original absolutePath. A symlink/relative path can pass containment as one path while the project switch later stores another. Also, unlike getProjectByPath, this bypasses the service’s existing PathValidator.

Suggested fix
-        if (!isUnderProjectsDir(projectDir)) {
+        val resolvedProjectDir = resolveProjectDirUnderProjectsDir(projectDir)
+        if (resolvedProjectDir == null) {
             Log.w(TAG, "[HOST] openProject denied: ${projectDir.absolutePath} is not under projects dir ${Environment.PROJECTS_DIR?.absolutePath}")
             throw SecurityException("Plugin $pluginId may only open projects under ${Environment.PROJECTS_DIR?.absolutePath}")
         }
 
-        if (!projectDir.exists() || !projectDir.isDirectory) {
-            Log.w(TAG, "[HOST] openProject aborted: not a directory (exists=${projectDir.exists()}, isDir=${projectDir.isDirectory})")
+        if (!isPathAllowed(resolvedProjectDir)) {
+            throw SecurityException("Plugin $pluginId does not have access to path: ${resolvedProjectDir.absolutePath}")
+        }
+
+        if (!resolvedProjectDir.exists() || !resolvedProjectDir.isDirectory) {
+            Log.w(TAG, "[HOST] openProject aborted: not a directory (exists=${resolvedProjectDir.exists()}, isDir=${resolvedProjectDir.isDirectory})")
             return false
         }
@@
-            ProjectManagerImpl.getInstance().projectPath = projectDir.absolutePath
-            GeneralPreferences.lastOpenedProject = projectDir.absolutePath
+            ProjectManagerImpl.getInstance().projectPath = resolvedProjectDir.absolutePath
+            GeneralPreferences.lastOpenedProject = resolvedProjectDir.absolutePath
@@
-    private fun isUnderProjectsDir(path: File): Boolean {
-        val projectsDir = runCatching { Environment.PROJECTS_DIR }.getOrNull() ?: return false
+    private fun resolveProjectDirUnderProjectsDir(path: File): File? {
+        val projectsDir = runCatching { Environment.PROJECTS_DIR }.getOrNull() ?: return null
         return runCatching {
             val base = projectsDir.canonicalFile
             val target = path.canonicalFile
-            target.path == base.path || target.path.startsWith(base.path + File.separator)
-        }.getOrDefault(false)
+            target.takeIf { it.path == base.path || it.path.startsWith(base.path + File.separator) }
+        }.getOrNull()
     }

Also applies to: 113-115, 129-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeProjectServiceImpl.kt`
around lines 97 - 105, Use the validated canonical project path in
IdeProjectServiceImpl.openProject and apply the existing PathValidator before
switching projects. The current flow checks containment with
isUnderProjectsDir(projectDir) but later persists the original path, so update
the openProject logic to reuse the validated canonical target and run it through
PathValidator like getProjectByPath does before calling the project
switch/selection code.

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.

2 participants