Skip to content

ADFA-4040: Disable misbehaving plugins instead of crashing the IDE#1319

Merged
Daniel-ADFA merged 6 commits into
stagefrom
ADFA-4040
May 22, 2026
Merged

ADFA-4040: Disable misbehaving plugins instead of crashing the IDE#1319
Daniel-ADFA merged 6 commits into
stagefrom
ADFA-4040

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

@Daniel-ADFA Daniel-ADFA commented May 19, 2026

When a plugin crashes, the crash is attributed to the offending plugin and absorbed rather than taking down the IDE. After three crashes the plugin is force-disabled and its UI contributions are removed.

  • forceDisablePlugin() now deactivates the plugin before disabling it
  • EditorHandlerActivity tears down a disabled plugin's editor tabs,
    sidebar items and menu entries so its features stop working
  • Replace the plugin-crash snackbar with a dialog; an info icon opens
    a scrollable view of the crash stack trace with copy-to-clipboard
  • PluginCrashedEvent now carries the stack trace from all crash sites
Screen_recording_20260519_171543.webm

  When a plugin throws, the crash is attributed to the offending plugin
  and absorbed rather than taking down the IDE. After three crashes the
  plugin is force-disabled and its UI contributions are removed.

  - forceDisablePlugin() now deactivates the plugin before disabling it
  - EditorHandlerActivity tears down a disabled plugin's editor tabs,
    sidebar items and menu entries so its features stop working
  - Replace the plugin-crash snackbar with a dialog; an info icon opens
    a scrollable view of the crash stack trace with copy-to-clipboard
  - PluginCrashedEvent now carries the stack trace from all crash sites
@Daniel-ADFA Daniel-ADFA requested a review from a team May 19, 2026 16:17
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b62cb388-906d-4ae5-986b-5670bbc999f5

📥 Commits

Reviewing files that changed from the base of the PR and between 990b879 and 686494d.

📒 Files selected for processing (1)
  • resources/src/main/res/values/strings.xml

📝 Walkthrough
  • Plugin Crash Resilience (ADFA-4040)
    • Overview

      • Plugin crashes are caught and attributed to the offending plugin instead of crashing the IDE.
      • After 3 recorded crashes a plugin is force-disabled and its UI contributions (editor tabs, sidebar items, menu entries) are removed.
      • Crash counts persist to disk (filesDir/plugin_crash_counts.properties) and survive restarts.
    • Key features

      • Main-thread Looper guard intercepts Looper.loop() exceptions, attempts plugin attribution, posts PluginCrashedEvent, and prevents process termination when the crash is attributable to a plugin.
      • PluginCrashedEvent now carries pluginId, pluginName, crashCount, wasDisabled, and the full stack trace.
      • UI: Crash snackbar replaced with a dialog (title/message/dev hint) and an info button that opens a scrollable stack-trace viewer with copy-to-clipboard support.
      • SafePluginLayoutInflater wraps plugin inflation, returns a programmatic error view on inflation failure, and invokes PluginFragmentHelper.onPluginInflationError.
      • PluginCrashTracker tracks per-plugin crash counts (in-memory ConcurrentHashMap + async persistence + Mutex) and decides when to force-disable (MAX_CRASH_COUNT = 3).
      • PluginManager integrates crash tracking: records crashes, forces disablement, deactivates before disabling, cleans up contributions, and exposes getLoadedPluginIds().
      • PluginEditorTabManager and EditorHandlerActivity include guarded handling and teardown for failed plugin contributions and disabled plugins (including removePluginTabs()).
      • EditorSidebarActions is made more fault-tolerant (skips unresolved plugins, wraps getSideMenuItems() in try/catch, logs and posts PluginCrashedEvent on failures, and avoids duplicate NavController listeners).
      • CredentialProtectedApplicationLoader wires the inflation-error hook and posts PluginCrashedEvent / tags Sentry scope when it attributes crashes to a plugin.
    • Notable implementation details

      • Attribution walks exception causes and stack frames and attempts Class.forName against each plugin's classloader to identify the originating plugin.
      • Crash persistence uses a simple properties file and writes asynchronously; persistence is serialized with a Mutex.
      • Many plugin lifecycle calls (deactivate(), dispose(), onDocumentationUninstall()) are run inside guarded blocks to avoid aborting flows on plugin exceptions.
    • Risks and best-practice violations

      • Looper.loop() interception is fragile and platform-sensitive; it may break across Android versions and does not cover native crashes/ANRs.
      • Exception suppression: swallowing errors to keep the IDE alive can obscure root causes and hinder debugging and telemetry.
      • Attribution fragility: Class.forName + classloader identity checks can misattribute crashes for obfuscated code, shared classloaders, or absent class names.
      • Persistence durability: asynchronous file writes during crash handling risk lost or inconsistent crash counts if the process terminates mid-write.
      • Partial cleanup risk: removing UI contributions may not release all plugin-held state; lingering references or race conditions are possible.
      • UX/theming: programmatically created error views from SafePluginLayoutInflater might not match app theming or accessibility expectations.
      • Storage choice: using a properties file instead of Android storage primitives (SharedPreferences/Room) may complicate concurrency, migration, and integrity guarantees.
      • User awareness: automatic force-disable may be insufficiently visible; users might not realize a plugin was disabled beyond the dialog message.
    • Testing recommendations

      • Verify crash-count persistence across forced terminations, process kills, and restarts.
      • Test attribution accuracy with obfuscated and third-party plugin classes and with plugins that share classloaders.
      • Exercise simultaneous crashes across multiple plugins and ensure crash counts and disable decisions behave correctly.
      • Validate UI teardown for disabled plugins to ensure no NPEs or leftover references (tabs, sidebar items, menu entries).
      • Exercise Looper guard with different exception types and confirm normal app lifecycle for non-plugin crashes.
      • Confirm SafePluginLayoutInflater fallback view respects theming and accessibility.
      • Test persistence edge-cases (concurrent writes, interrupted writes) and consider adding stronger durability/transaction semantics if needed.

Walkthrough

Adds end-to-end plugin crash handling: per-plugin crash tracking with persistence, safe plugin layout inflation, app-level Looper-based crash attribution, resilient plugin lifecycle/tab/sidebar handling, user-facing crash dialogs with log viewing/copying, and automated teardown/reload when plugins become disabled.

Changes

Plugin crash detection and recovery system

Layer / File(s) Summary
Crash event and tracking infrastructure
eventbus-events/.../PluginCrashedEvent.kt, plugin-manager/.../PluginCrashTracker.kt
New PluginCrashedEvent and PluginCrashTracker for per-plugin crash counts, properties-file persistence, record/reset APIs, disable threshold, and stack-trace attribution via classloader inspection.
Safe plugin layout inflation with error reporting
plugin-api/.../PluginFragment.kt, plugin-api/.../SafePluginLayoutInflater.kt
PluginFragmentHelper.onPluginInflationError and SafePluginLayoutInflater.wrap(...) catch inflation exceptions, invoke the error callback, and return a programmatic error view instead of throwing.
PluginManager crash integration and lifecycle safety
plugin-manager/.../PluginManager.kt, plugin-manager/.../PluginDocumentationManager.kt
Adds crashTracker, safe unload/disable/dispose handling, uninstall resets/removes crash counts, enablement resets counts, and public crash APIs: recordPluginCrash, forceDisablePlugin, CrashResult, and getLoadedPluginIds(). Documentation uninstall callback wrapped in a safe block.
App-level crash detection and attribution
app/src/main/java/.../CredentialProtectedApplicationLoader.kt
Installs a main-thread Looper guard to catch uncaught exceptions, attempts to map stack traces to plugins via PluginCrashTracker, records plugin crashes (tags Sentry, posts PluginCrashedEvent) without killing the process, delegates to original handler for non-plugin crashes, and wires inflation-error handler before async plugin loading.
Editor tab operation crash resilience
plugin-manager/.../PluginEditorTabManager.kt
Wraps plugin tab extension operations (tab enumeration, fragment factory, selection, close-check, close callback) in try/catch; records crashes and posts PluginCrashedEvent while returning safe defaults and continuing cleanup. Adds removePluginTabs(pluginId).
Sidebar registration crash resilience and crash UI recovery
app/src/main/java/.../EditorSidebarActions.kt, app/src/main/java/.../EditorHandlerActivity.kt, app/src/main/res/layout/dialog_plugin_crash.xml, app/src/main/res/layout/snackbar_custom.xml, resources/src/main/res/values/strings.xml
Sidebar registration skips unresolved IDs, wraps getSideMenuItems() in try/catch, warns on extra items, records crashes and posts PluginCrashedEvent. EditorHandlerActivity subscribes to PluginCrashedEvent, shows crash dialog and log viewer (copy-to-clipboard); when a plugin is disabled it tears down/reloads plugin tabs and sidebar actions, reinitializes the sidebar fragment, and invalidates the options menu. New dialog layout, multiline snackbar support, and strings added.

Sequence Diagrams

sequenceDiagram
  participant Thread as Main Thread Looper
  participant Guard as PluginCrashLooperGuard
  participant Tracker as PluginCrashTracker
  participant Manager as PluginManager
  participant EventBus
  participant Sentry as Sentry
  
  Thread->>Guard: uncaught exception thrown
  Guard->>Tracker: findPluginForStackTrace(throwable)
  Tracker-->>Guard: pluginId or null
  alt Plugin crash detected
    Guard->>Manager: recordPluginCrash(pluginId)
    Manager->>Tracker: recordCrash(pluginId)
    Tracker-->>Manager: crashCount
    Manager->>Sentry: captureException + set plugin tags
    Manager->>EventBus: post PluginCrashedEvent(pluginId, ...)
    Guard-->>Guard: continue (do not exit)
  else Non-plugin crash
    Guard->>Guard: call original uncaughtExceptionHandler
    Guard->>Thread: exit process
  end
Loading
sequenceDiagram
  participant EventBus
  participant EditorHandler as EditorHandlerActivity
  participant UI as Crash Dialog UI
  participant PluginMgr as PluginManager
  participant TabMgr as PluginEditorTabManager
  participant SidebarMgr as EditorSidebarActions
  participant Fragment as EditorSidebarFragment
  
  EventBus->>EditorHandler: PluginCrashedEvent
  EditorHandler->>UI: showPluginCrashDialog(event)
  UI-->>EditorHandler: user opens log viewer / copies stack trace
  
  alt Plugin was disabled
    EditorHandler->>PluginMgr: getLoadedPluginIds()
    EditorHandler->>TabMgr: removePluginTabs(pluginId)
    EditorHandler->>TabMgr: reloadPluginTabs()
    EditorHandler->>SidebarMgr: clearEditorSidebarActions()
    EditorHandler->>SidebarMgr: registerPluginSidebarActions()
    EditorHandler->>Fragment: reinitialize sidebar fragment (if present)
    EditorHandler->>EditorHandler: invalidateOptionsMenu()
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • dara-abijo-adfa
  • jatezzz
  • jomen-adfa

Poem

🐰 I nibble logs beneath the screen,
I patch the cracks that go unseen.
When plugins trip and layouts sigh,
I show the trace and let you try.
Copy, reload — the IDE's sky high.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.69% 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 'ADFA-4040: Disable misbehaving plugins instead of crashing the IDE' clearly and concisely summarizes the main change: implementing plugin crash handling to prevent IDE crashes by disabling problematic plugins.
Description check ✅ Passed The description is directly related to the changeset, detailing key features like crash attribution, plugin disablement after three crashes, teardown of plugin contributions, crash dialog with stack trace viewer, and PluginCrashedEvent updates.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4040

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 and usage tips.

Copy link
Copy Markdown
Contributor

@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: 8

🤖 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/EditorHandlerActivity.kt`:
- Around line 1177-1180: EditorSidebarActions.setup currently adds a
NavController.OnDestinationChangedListener every time it's called, causing
listeners to accumulate; modify setup(EditorSidebarFragment) to either (a) store
the added listener (or a flag) on the fragment or in a weak map keyed by the
fragment or its NavController and remove the previous listener via
navController.removeOnDestinationChangedListener(...) before adding the new one,
or (b) detect that setup has already been run for that fragment/controller and
skip re-attaching; update EditorSidebarActions.setup and any helper storage to
track and/or remove the existing NavController.OnDestinationChangedListener for
the given EditorSidebarFragment/NavController to prevent duplicate callbacks and
stale references.

In
`@app/src/main/java/com/itsaky/androidide/app/CredentialProtectedApplicationLoader.kt`:
- Around line 158-163: Replace the persistent use of Sentry.configureScope in
CredentialProtectedApplicationLoader (where scope.setTag("plugin_crash", "true")
and scope.setTag("plugin_id", pluginId) are set before
Sentry.captureException(exception)) with Sentry.withScope so the tags apply only
to this single capture; inside the withScope block set the two tags on the
provided scope and call Sentry.captureException(exception) from within that
block to ensure the tags do not leak to subsequent events.

In `@app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt`:
- Around line 267-279: Change the broad catch in EditorSidebarActions around
getSideMenuItems() to catch Exception instead of Throwable: locate the try/catch
that logs "Plugin '$pluginId' crashed in getSideMenuItems()" and adjust the
catch parameter type from Throwable to Exception so unrecoverable errors (e.g.,
OutOfMemoryError/LinkageError) are not swallowed; keep the existing logging,
pluginManager.recordPluginCrash(pluginId) handling, crashCount computation, and
EventBus.post(PluginCrashedEvent(...)) behavior unchanged.

In
`@plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/SafePluginLayoutInflater.kt`:
- Around line 19-21: The override onCreateView currently only calls
createView(name, "android.widget.", attrs) which breaks unqualified framework
tags; change onCreateView to first attempt createView(name, "android.view.",
attrs) and if that returns null, fall back to createView(name,
"android.widget.", attrs) (optionally also consider "android.webkit." for
WebView) so the normal framework prefix fallback behavior is restored in
SafePluginLayoutInflater.
- Around line 27-33: The inflate method in SafePluginLayoutInflater currently
catches Throwable which hides fatal VM errors; update the catch clause in
SafePluginLayoutInflater.inflate(resource: Int, root: ViewGroup?, attachToRoot:
Boolean): View to catch Exception instead of Throwable (i.e., catch Exception e)
so Errors (OutOfMemoryError, LinkageError, etc.) are allowed to propagate; keep
the existing PluginFragmentHelper.onPluginInflationError?.invoke(pluginId, e)
and createErrorView(root) logic inside the Exception handler unchanged.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 883-901: forceDisablePlugin currently only calls deactivate() and
flips isEnabled, leaving manager-owned registrations intact; update
forceDisablePlugin to also perform the same teardown path as unloadPlugin so
registrations from activateLoadedPlugin (e.g., PluginSnippetManager,
PluginBuildActionManager, command/template services) are removed. Concretely,
either call the existing unloadPlugin(pluginId) (or extract the shared cleanup
logic from unloadPlugin into a new private
cleanup/unregisterLoadedPlugin(pluginId) helper and invoke it from both
unloadPlugin and forceDisablePlugin) so that all manager-registered integrations
are unregistered when a plugin is force-disabled. Ensure the crash logging and
state persistence (savePluginState) remain consistent after the change.
- Around line 893-905: CrashResult.Recorded currently omits the plugin display
name causing the UI to show raw IDs for non-disabled crashes; update
recordPluginCrash to include the manifest name when returning
CrashResult.Recorded by changing the Recorded variant to carry the pluginName
and have recordPluginCrash read loadedPlugins[pluginId]?.manifest?.name ?:
pluginId just like the Disabled path so both CrashResult.Recorded and
CrashResult.Disabled include pluginName.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 127-129: The current use of runCatching around
plugin?.onDocumentationUninstall() swallows CancellationException; replace it
with an explicit try/catch that rethrows CancellationException and only handles
other Throwables so coroutine cancellations propagate correctly. Locate the call
in PluginDocumentationManager (the plugin?.onDocumentationUninstall()
invocation) and follow the same pattern used elsewhere in this file: wrap the
call in try { plugin?.onDocumentationUninstall() } catch (e:
CancellationException) { throw e } catch (e: Throwable) { Log.e(TAG, "Plugin
onDocumentationUninstall() threw during removal: $pluginId", e) } to preserve
structured concurrency.
🪄 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: d482c853-149c-4ab7-8ddf-6308f185a43d

📥 Commits

Reviewing files that changed from the base of the PR and between a3e67cb and 603494c.

📒 Files selected for processing (13)
  • app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
  • app/src/main/java/com/itsaky/androidide/app/CredentialProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt
  • app/src/main/res/layout/dialog_plugin_crash.xml
  • app/src/main/res/layout/snackbar_custom.xml
  • eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/plugin/PluginCrashedEvent.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/PluginFragment.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/SafePluginLayoutInflater.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginCrashTracker.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt
  • resources/src/main/res/values/strings.xml

Comment thread app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt Outdated
Copy link
Copy Markdown
Contributor

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt (1)

208-215: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep host tab selection running after a plugin hook crash.

Line 213 returns before tabSelectionListener?.onTabSelected(tabId, fragment), so a plugin exception turns tab selection into a no-op even though the crash was already absorbed. Please record the crash, but still let the host finish switching tabs.

Suggested minimal fix
             try {
                 tabInfo.extension.onEditorTabSelected(tabId, fragment)
             } catch (e: Throwable) {
                 val pluginId = resolvePluginId(tabInfo.extension)
                 if (pluginId != null) handlePluginCrash(pluginId, "onEditorTabSelected()", e)
-                return
             }
             tabSelectionListener?.onTabSelected(tabId, fragment)
             logger.debug("Tab selection handled for plugin tab: {}", tabId)
🤖 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/ui/PluginEditorTabManager.kt`
around lines 208 - 215, A plugin exception in PluginEditorTabManager prevents
the host tab selection from completing because the catch block returns early;
instead, in the try/catch around tabInfo.extension.onEditorTabSelected(tabId,
fragment) call, record the crash via resolvePluginId(...) and
handlePluginCrash(pluginId, "onEditorTabSelected()", e) if pluginId != null but
do NOT return—allow execution to continue so
tabSelectionListener?.onTabSelected(tabId, fragment) still runs; update the
catch block in PluginEditorTabManager to remove the early return and only
absorb/log the error.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt (1)

870-880: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the plugin state consistent if deactivate() fails.

If deactivate() throws on Line 872, cleanupPluginContributions() has already removed snippets/build actions/templates, but isEnabled and the persisted state never flip to false. That leaves the plugin half-disabled while the manager still reports it enabled. disablePlugin() should mirror forceDisablePlugin() here: treat deactivation as best-effort and always persist the disabled state once cleanup has started.

Suggested fix
 fun disablePlugin(pluginId: String): Boolean {
     val loadedPlugin = loadedPlugins[pluginId] ?: return false

     if (!loadedPlugin.isEnabled) {
         logger.info("Plugin $pluginId is already disabled")
         return true
     }

-    return try {
-        cleanupPluginContributions(loadedPlugin)
-        loadedPlugin.plugin.deactivate()
-        loadedPlugin.isEnabled = false
-        savePluginState(pluginId, false)
-
-        logger.info("Disabled plugin: $pluginId")
-        true
-    } catch (e: Exception) {
-        logger.error("Failed to disable plugin: $pluginId", e)
-        false
-    }
+    cleanupPluginContributions(loadedPlugin)
+    runCatching { loadedPlugin.plugin.deactivate() }
+        .onFailure { logger.error("Plugin deactivate threw during disable: $pluginId", it) }
+
+    loadedPlugin.isEnabled = false
+    savePluginState(pluginId, false)
+    logger.info("Disabled plugin: $pluginId")
+    return true
 }
🤖 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 870 - 880, The disablePlugin() flow can leave a plugin half-enabled
if loadedPlugin.plugin.deactivate() throws: cleanupPluginContributions() runs
but loadedPlugin.isEnabled and savePluginState(pluginId, false) are not
executed; change disablePlugin() to treat deactivate() as best-effort (like
forceDisablePlugin()) by ensuring that once
cleanupPluginContributions(loadedPlugin) has been called you always set
loadedPlugin.isEnabled = false and call savePluginState(pluginId, false)
regardless of whether loadedPlugin.plugin.deactivate() throws (e.g., move those
state changes to a finally block or perform them after catching exceptions from
deactivate()), while still logging deactivate() errors via logger.error.
🤖 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.

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 870-880: The disablePlugin() flow can leave a plugin half-enabled
if loadedPlugin.plugin.deactivate() throws: cleanupPluginContributions() runs
but loadedPlugin.isEnabled and savePluginState(pluginId, false) are not
executed; change disablePlugin() to treat deactivate() as best-effort (like
forceDisablePlugin()) by ensuring that once
cleanupPluginContributions(loadedPlugin) has been called you always set
loadedPlugin.isEnabled = false and call savePluginState(pluginId, false)
regardless of whether loadedPlugin.plugin.deactivate() throws (e.g., move those
state changes to a finally block or perform them after catching exceptions from
deactivate()), while still logging deactivate() errors via logger.error.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt`:
- Around line 208-215: A plugin exception in PluginEditorTabManager prevents the
host tab selection from completing because the catch block returns early;
instead, in the try/catch around tabInfo.extension.onEditorTabSelected(tabId,
fragment) call, record the crash via resolvePluginId(...) and
handlePluginCrash(pluginId, "onEditorTabSelected()", e) if pluginId != null but
do NOT return—allow execution to continue so
tabSelectionListener?.onTabSelected(tabId, fragment) still runs; update the
catch block in PluginEditorTabManager to remove the early return and only
absorb/log the error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cc693550-a7d6-4a83-9b79-d653fdb5cacf

📥 Commits

Reviewing files that changed from the base of the PR and between 603494c and f08c52b.

📒 Files selected for processing (6)
  • app/src/main/java/com/itsaky/androidide/app/CredentialProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/utils/EditorSidebarActions.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/base/SafePluginLayoutInflater.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt

Copy link
Copy Markdown
Contributor

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt (1)

1530-1545: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don’t re-enter plugin code while tearing down sidebar actions.

Line 1540 calls plugin.getSideMenuItems() during force-disable/uninstall. If the crash came from that code path, cleanup aborts and the disabled plugin keeps stale sidebar entries. This teardown needs manager-owned/cached action IDs instead of recomputing them from the plugin again.

🤖 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 1530 - 1545, The teardown currently calls plugin.getSideMenuItems()
inside cleanupSidebarActions, which re-enters plugin code and can crash
teardown; instead store manager-owned sidebar action IDs when the plugin is
registered and use that cache here. Update the registration/loading path that
handles UIExtension to compute and save the action IDs (e.g.,
"plugin_sidebar_<id>") into the loadedPlugins entry (or a new sidebarActionIds
field) and then change cleanupSidebarActions to read
loadedPlugins[pluginId]?.sidebarActionIds (not plugin.getSideMenuItems()) and
iterate those IDs to call ActionsRegistry.unregisterAction; ensure cleanup still
logs successes and handles a missing cache gracefully.
🤖 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
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 1057-1063: In removePluginState (PluginManager), the
prefsFile.inputStream() and prefsFile.outputStream() are opened without closing;
update the code to wrap the input and output streams with Kotlin's use() so the
streams are closed reliably (e.g., call prefsFile.inputStream().use {
properties.load(it) } and prefsFile.outputStream().use { properties.store(it,
"Plugin states") }) around the properties.load and properties.store calls,
keeping the same Properties instance and preserving the logger.debug call
afterwards.
- Around line 893-897: The force-disable path currently removes tab
registrations but doesn't unregister the plugin's fragment classloaders,
allowing fragments to be recreated from saved state; update the force-disable
sequence (after cleanupSidebarActions and before/after
cleanupPluginContributions) to call unloadPlugin(pluginId) (or the existing
method that unregisters fragment factories) and wrap it in runCatching {
unloadPlugin(pluginId) }.onFailure { e -> logger.error("Failed to unload plugin
during force-disable: $pluginId", e) } so fragment factories/classloaders are
properly removed; ensure you reference loadedPlugin/pluginId consistently and
preserve existing error handling.

---

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 1530-1545: The teardown currently calls plugin.getSideMenuItems()
inside cleanupSidebarActions, which re-enters plugin code and can crash
teardown; instead store manager-owned sidebar action IDs when the plugin is
registered and use that cache here. Update the registration/loading path that
handles UIExtension to compute and save the action IDs (e.g.,
"plugin_sidebar_<id>") into the loadedPlugins entry (or a new sidebarActionIds
field) and then change cleanupSidebarActions to read
loadedPlugins[pluginId]?.sidebarActionIds (not plugin.getSideMenuItems()) and
iterate those IDs to call ActionsRegistry.unregisterAction; ensure cleanup still
logs successes and handles a missing cache gracefully.
🪄 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: 56ec759f-4794-4d97-af29-57a46f66185a

📥 Commits

Reviewing files that changed from the base of the PR and between f08c52b and 978c545.

📒 Files selected for processing (3)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginCrashTracker.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginEditorTabManager.kt

@Daniel-ADFA Daniel-ADFA merged commit cf059bd into stage May 22, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-4040 branch May 22, 2026 12:51
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.

3 participants