Skip to content

ADFA-3580: (feat) Plugin Build Actions & Custom Scripts System#1150

Open
Daniel-ADFA wants to merge 2 commits intostagefrom
ADFA-3580
Open

ADFA-3580: (feat) Plugin Build Actions & Custom Scripts System#1150
Daniel-ADFA wants to merge 2 commits intostagefrom
ADFA-3580

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

Adds first-class API for plugins to declare build actions (shell commands and Gradle tasks), execute them with streaming output to the Build Output panel, and control toolbar action visibility.

Screen.Recording.2026-04-03.at.01.13.52.mov

  Adds first-class API for plugins to declare build actions (shell commands
  and Gradle tasks), execute them with streaming output to the Build Output
  panel, and control toolbar action visibility.

  Includes custom scripts support via .codeonthego/scripts.json — plugins
  can auto-detect project type (Node.js, Python, Rust, Go, Make, Ruby)
  and bootstrap user-editable run scripts on first project open.

  New plugin-api interfaces: BuildActionExtension, IdeCommandService,
  CommandExecution. New plugin-manager implementations:
  IdeCommandServiceImpl (ProcessBuilder-based with Termux env injection),
  PluginBuildActionManager (singleton orchestrator). New app integration:
  PluginBuildActionItem
  Adds first-class API for plugins to declare build actions (shell commands
  and Gradle tasks), execute them with streaming output to the Build Output
  panel, and control toolbar action visibility.

  Includes custom scripts support via .codeonthego/scripts.json — plugins
  can auto-detect project type (Node.js, Python, Rust, Go, Make, Ruby)
  and bootstrap user-editable run scripts on first project open.

  New plugin-api interfaces: BuildActionExtension, IdeCommandService,
  CommandExecution. New plugin-manager implementations:
  IdeCommandServiceImpl (ProcessBuilder-based with Termux env injection),
  PluginBuildActionManager (singleton orchestrator). New app integration:
  PluginBuildActionItem
@Daniel-ADFA Daniel-ADFA requested a review from a team April 3, 2026 00:15
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 3, 2026

📝 Walkthrough

Release Notes: Plugin Build Actions & Custom Scripts System

New Features

  • Plugin Build Actions API: Introduces BuildActionExtension interface allowing plugins to declare build actions with support for shell commands and Gradle tasks
  • Build Action Execution Service: New IdeCommandService providing controlled command execution with streaming output (stdout/stderr/exit codes) via Flow-based API
  • Build Output Integration: Plugin-executed commands stream output to the Build Output panel with progress indicators in the editor
  • Toolbar Visibility Control: Plugins can declaratively hide toolbar actions via toolbarActionsToHide() method
  • Manifest-Based Build Actions: Support for declaring build actions in plugin manifests (build_actions field) alongside programmatic extension registration
  • Command Lifecycle Hooks: Optional onActionStarted() and onActionCompleted() callbacks for plugins to react to action execution events
  • Build Action UI Component: New PluginBuildActionItem toolbar action that dynamically displays plugin actions with running/cancel states

Technical Implementation

  • Concurrent Execution Management: Limits active commands to 3 concurrent executions with proper queueing
  • Output Buffering: Captures stdout/stderr with 10MB per-stream limit to prevent unbounded memory growth
  • Timeout Support: Configurable per-action timeout (default 600 seconds) with graceful process termination
  • Gradle Task Support: Native integration with Gradle tasks via gradlew discovery from project root
  • Termux Environment Injection: Automatic PATH/LD_LIBRARY_PATH/environment setup for sandboxed execution environments
  • Process Cleanup: Forcible process destruction on cancellation or timeout with proper resource release
  • Plugin Lifecycle Integration: Automatic cleanup of active commands and registrations on plugin unload

API Changes

  • New plugin-api dependency: kotlinx-coroutines-core (1.9.0) exposed as public API
  • New public interfaces: BuildActionExtension, IdeCommandService, CommandExecution
  • New data models: PluginBuildAction, CommandSpec, CommandOutput, CommandResult, BuildActionCategory
  • New implementation classes: IdeCommandServiceImpl, PluginBuildActionManager, PluginBuildActionItem

Risk Considerations

⚠️ Security: Commands require SYSTEM_COMMANDS permission; working directories for shell commands are validated to remain within project root canonical path

⚠️ Resource Limits: Output buffering capped at 10MB per stream; concurrent commands limited to 3—plugins executing long-running or high-volume output commands may experience truncation

⚠️ Error Handling: Process failures include error text capture; timeout errors are reported as exit code -1 with descriptive timeout message

⚠️ Thread Safety: Concurrent registries used for action tracking; ensure plugins handle race conditions in onActionCompleted() callbacks

⚠️ Exception Handling Improvement: PluginDrawableResolver now catches broader Throwable instead of just Resources.NotFoundException—verify fallback behavior covers expected error cases

Walkthrough

This pull request introduces a comprehensive plugin build action system, enabling plugins to register and execute custom build actions within the Android IDE editor. The system includes a new plugin API contract, command execution service, centralized action manager, and editor toolbar integration for displaying and executing plugin-defined build actions.

Changes

Cohort / File(s) Summary
Plugin API Contracts
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/BuildActionExtension.kt, plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeCommandService.kt
Introduces BuildActionExtension interface for plugins to define build actions and lifecycle hooks, along with CommandSpec/CommandOutput/CommandResult data models for command specifications and execution results. Defines IdeCommandService interface for controlled command execution with execution tracking and cancellation support.
Command Service Implementation
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt
Implements IdeCommandService with process-based command execution, concurrent execution limits, timeout handling, and streaming output via coroutine-based CommandExecutionImpl. Supports both shell commands and Gradle task execution with restricted working directory and environment injection.
Build Action Management
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt
Central singleton managing plugin-registered and manifest-derived build actions, tracking active command executions, coordinating action lifecycle events, and providing toolbar visibility control via toolbarActionsToHide().
Plugin System Integration
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
Extends PluginManager to register build actions from plugins and manifest definitions, provision IdeCommandService to plugin contexts, and cleanup actions on plugin unload. Adds ManifestBuildAction data class and buildActions property to PluginManifest for declarative action definitions.
Editor Toolbar Integration
app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt, app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt, app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt
Introduces PluginBuildActionItem action class for rendering plugin build actions in the toolbar with running/cancelled states, integrates plugin action registration and visibility filtering in EditorActivityActions, and filters toolbar actions by plugin-hidden IDs in EditorHandlerActivity.prepareOptionsMenu().
Supporting Changes
plugin-api/build.gradle.kts, app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt
Adds kotlinx-coroutines-core as API dependency, adds early return in EditorActivityAction.prepare() when context is missing, and broadens exception handling in PluginDrawableResolver.resolve().

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Editor as Editor Activity
    participant Action as PluginBuildActionItem
    participant Manager as PluginBuildActionManager
    participant Service as IdeCommandService
    participant Process as Process/Executor
    
    User->>Editor: Clicks plugin build action
    Editor->>Action: execAction(data)
    Action->>Manager: isActionRunning(pluginId, actionId)
    Manager-->>Action: false
    Action->>Manager: executeAction(pluginId, actionId, service)
    Manager->>Manager: onActionStarted(actionId)
    Manager->>Service: executeCommand(CommandSpec)
    Service->>Process: spawn process / task
    Process-->>Service: output stream
    Service-->>Manager: CommandExecution(output flow, await, cancel)
    Manager-->>Action: CommandExecution
    Action->>Action: mark build in-progress
    Action->>Action: toggle bottom sheet to build output
    
    par Concurrent Output Collection
        Action->>Service: collect output flow
        Service->>Process: read stdout/stderr
        Process-->>Service: output lines
        Service-->>Action: emit CommandOutput events
        Action->>Action: append lines to UI
    and Process Waits
        Process-->>Service: completion/timeout
        Service->>Manager: notifyActionCompleted(result)
        Manager->>Manager: onActionCompleted(actionId, result)
        Manager-->>Action: action cleanup
        Action->>Action: reset progress state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa
  • jomen-adfa
  • jatezzz

🐰 Plugins build with verve, in actions so bright,
Toolbar buttons dance, executing just right,
With command flows streaming and timeouts so keen,
The IDE now sings with a builder's machine! ✨🔨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically identifies the main feature added: a plugin build actions and custom scripts system, which aligns with the changeset's primary objective.
Description check ✅ Passed The description is directly related to the changeset, explaining the core feature of enabling plugins to declare and execute build actions with streaming output and toolbar visibility control.

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

✨ 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 ADFA-3580

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

🧹 Nitpick comments (8)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt (1)

18-19: Intermediate variable is unnecessary.

The local drawable variable adds no value here—consider returning directly for conciseness.

♻️ Suggested simplification
             try {
-                val drawable = ContextCompat.getDrawable(pluginContext, resId)
-                return drawable
+                return ContextCompat.getDrawable(pluginContext, resId)
             } catch (_: Resources.NotFoundException) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt`
around lines 18 - 19, In PluginDrawableResolver (the method that currently does
"val drawable = ContextCompat.getDrawable(pluginContext, resId); return
drawable"), remove the unnecessary local variable and return the result of
ContextCompat.getDrawable(pluginContext, resId) directly to simplify the code
and improve conciseness; update the method body to a single direct return of
ContextCompat.getDrawable(pluginContext, resId).
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeCommandService.kt (1)

8-20: Consider adding KDoc for the public plugin API.

This is a new public API surface for plugin developers. Adding documentation would improve discoverability and usage:

📝 Proposed documentation additions
+/**
+ * Service for executing shell commands and Gradle tasks from plugins.
+ */
 interface IdeCommandService {
+    /**
+     * Starts command execution with the given specification.
+     * `@param` spec The command to execute (shell command or Gradle task)
+     * `@param` timeoutMs Maximum execution time in milliseconds (default: 10 minutes)
+     * `@return` A CommandExecution handle for monitoring and controlling the execution
+     */
     fun executeCommand(spec: CommandSpec, timeoutMs: Long = 600_000): CommandExecution
     fun isCommandRunning(executionId: String): Boolean
     fun cancelCommand(executionId: String): Boolean
     fun getRunningCommandCount(): Int
 }

+/**
+ * Handle for a running command execution.
+ */
 interface CommandExecution {
+    /** Unique identifier for this execution */
     val executionId: String
+    /** Flow emitting stdout, stderr, and exit code as they become available */
     val output: Flow<CommandOutput>
+    /** Suspends until the command completes and returns the result */
     suspend fun await(): CommandResult
+    /** Requests cancellation of the running command */
     fun cancel()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeCommandService.kt`
around lines 8 - 20, Add KDoc comments describing the public API surface:
document the IdeCommandService interface and each method (executeCommand,
isCommandRunning, cancelCommand, getRunningCommandCount) and the
CommandExecution interface and its members (executionId, output, await, cancel)
to explain purpose, parameters, return types, default values (e.g., timeoutMs
default 600_000), possible exceptions/behaviour (cancellation semantics,
lifecycle of output Flow), and usage examples or thread-safety notes as
appropriate to help plugin authors discover and use these APIs.
app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt (1)

135-140: Clarify the relationship between buildService and plugin action state.

resetProgressIfIdle checks buildService?.isBuildInProgress but plugin build actions don't appear to update buildService state. This could lead to inconsistent progress indicators if a Gradle build runs concurrently with a plugin action. Consider either:

  • Having plugin actions update buildService.isBuildInProgress
  • Or tracking plugin action progress separately with clear documentation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt`
around lines 135 - 140, resetProgressIfIdle currently only checks
buildService?.isBuildInProgress which is never updated by plugin actions; either
update the plugin action lifecycle to flip buildService.isBuildInProgress when a
plugin build starts/ends (set buildService?.isBuildInProgress = true at action
start and false at completion in the PluginBuildActionItem action handlers) or
add a separate flag (e.g.,
EditorHandlerActivity.editorViewModel.isPluginActionInProgress) and change
resetProgressIfIdle to consider both buildService?.isBuildInProgress and
isPluginActionInProgress so UI progress reflects both Gradle builds and plugin
actions consistently.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt (1)

61-75: Consider adding @SerializedName annotations for consistency.

The ManifestBuildAction data class has inconsistent serialization annotations. Some fields use @SerializedName (e.g., gradle_task, working_directory, timeout_ms) while others rely on Gson's default field name matching. For maintainability and clarity about the expected JSON schema, consider adding explicit annotations to all fields.

♻️ Proposed fix for consistent annotations
 data class ManifestBuildAction(
+    `@SerializedName`("id")
     val id: String,
+    `@SerializedName`("name")
     val name: String,
+    `@SerializedName`("description")
     val description: String = "",
+    `@SerializedName`("category")
     val category: String = "CUSTOM",
+    `@SerializedName`("command")
     val command: String? = null,
+    `@SerializedName`("arguments")
     val arguments: List<String> = emptyList(),
     `@SerializedName`("gradle_task")
     val gradleTask: String? = null,
     `@SerializedName`("working_directory")
     val workingDirectory: String? = null,
+    `@SerializedName`("environment")
     val environment: Map<String, String> = emptyMap(),
     `@SerializedName`("timeout_ms")
     val timeoutMs: Long = 600_000
 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt`
around lines 61 - 75, ManifestBuildAction has mixed use of `@SerializedName` which
can confuse the expected JSON schema; update the data class by adding
`@SerializedName` annotations for all properties (id, name, description, category,
command, arguments, environment) to match the JSON field names you expect (e.g.,
"id", "name", "description", "category", "command", "arguments", "environment")
while retaining the existing annotations for gradleTask ("gradle_task"),
workingDirectory ("working_directory") and timeoutMs ("timeout_ms") so
serialization is consistent and explicit across the class.
app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt (1)

192-193: Silent exception swallowing may hide plugin registration issues.

Per project learnings, prefer narrow exception handling over broad catch-all. Silently swallowing exceptions makes debugging plugin registration failures difficult. Consider logging or catching only expected exception types.

♻️ Proposed fix to add minimal logging
-                } catch (_: Exception) {
+                } catch (e: Exception) {
+                    Log.w("plugin_debug", "Failed to register menu items for plugin: ${plugin.javaClass.simpleName}", e)
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt`
around lines 192 - 193, The empty catch block swallowing all Exceptions should
be replaced: catch only the expected exception types thrown during plugin
registration (e.g., IllegalStateException/IOException or the concrete exceptions
your plugin APIs throw) or, if you must catch Exception, log the error instead
of ignoring it; update the catch in EditorActivityActions.kt (the catch (_:
Exception) { } block used around plugin registration) to call the project
logging utility (or Android Log.e/Timber) with a clear message like "plugin
registration failed" and include the exception so failures are visible for
debugging.
plugin-api/build.gradle.kts (1)

32-33: Using api() is correct; consider upgrading to kotlinx-coroutines 1.10.0+.

The api configuration is appropriate since Flow<CommandOutput> is exposed in the public IdeCommandService interface. Coroutines 1.9.0 is compatible with your project's Kotlin 2.1.21 and poses no compatibility issues. However, for Kotlin 2.1 projects, kotlinx-coroutines 1.10.0+ is the recommended version.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugin-api/build.gradle.kts` around lines 32 - 33, Keep the api configuration
but update the kotlinx-coroutines dependency version: replace the
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") entry with a 1.10.0+
version (e.g., 1.10.0 or later) to follow Kotlin 2.1.21 recommendations and
ensure compatibility for the public Flow<CommandOutput> in IdeCommandService.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt (2)

50-56: Validate that gradlew exists and is executable before use.

The code assumes gradlew exists in the project root but doesn't verify this. If the wrapper is missing or not executable, the process will fail with a potentially confusing error.

🛠️ Proposed fix
             is CommandSpec.GradleTask -> {
                 val gradleWrapper = projectRoot?.let { File(it, "gradlew") }
                     ?: throw IllegalStateException("No project root available for Gradle task execution")
+                if (!gradleWrapper.exists()) {
+                    throw IllegalStateException("Gradle wrapper not found at ${gradleWrapper.absolutePath}")
+                }
+                if (!gradleWrapper.canExecute()) {
+                    throw IllegalStateException("Gradle wrapper is not executable: ${gradleWrapper.absolutePath}")
+                }
                 ProcessBuilder(listOf(gradleWrapper.absolutePath, spec.taskPath) + spec.arguments).apply {
                     directory(projectRoot)
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt`
around lines 50 - 56, CommandSpec.GradleTask currently assumes gradlew exists
and is executable; modify the block that builds gradleWrapper so you explicitly
validate File(projectRoot, "gradlew") exists and isFile and isExecutable (or
attempt gradleWrapper.setExecutable(true) and re-check), and if those checks
fail throw a clear IllegalStateException mentioning missing or non-executable
gradlew; then proceed to construct the ProcessBuilder with
gradleWrapper.absolutePath, spec.taskPath and spec.arguments as before. Ensure
you reference gradleWrapper, projectRoot, CommandSpec.GradleTask and
ProcessBuilder when locating and updating the code.

85-88: Minor race window between cancellation and clear.

A command could be added between forEach completing and clear() being called, causing it to be silently removed without cancellation. Consider iterating until the map is empty or using removeIf.

♻️ Proposed fix
     fun cancelAllCommands() {
-        runningCommands.values.forEach { it.cancel() }
-        runningCommands.clear()
+        runningCommands.entries.removeAll { (_, execution) ->
+            execution.cancel()
+            true
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt`
around lines 85 - 88, cancelAllCommands has a race where a command can be added
between iterating and clear(); fix by making the cancel-and-clear atomic: wrap
the loop and clear in a single synchronized block (or use a dedicated mutex) so
that runningCommands.values.forEach { it.cancel() } and runningCommands.clear()
execute under the same lock; update the cancelAllCommands implementation to
synchronize on the runningCommands instance (or the class-level lock) to prevent
new entries being added while cancelling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt`:
- Around line 42-62: The override of prepare in PluginBuildActionItem is missing
a call to the base implementation; call super.prepare(data) at the start of
PluginBuildActionItem.prepare so BaseBuildAction.prepare can perform its context
validation and set visible/enabled based on buildService state before you apply
plugin-specific logic (or if you intentionally want to bypass that behavior, add
a comment explaining the reason and consider extracting a different base class
rather than skipping super.prepare); keep the rest of the method (using
PluginBuildActionManager.isActionRunning and setting label/icon/enabled)
unchanged.
- Around line 112-130: The coroutine launched on actionScope can outlive the
Activity and call withContext(Dispatchers.Main) after the Activity is destroyed;
change the launch to use the Activity lifecycle (replace actionScope.launch with
activity.lifecycleScope.launch or otherwise tie the Job to activity.lifecycle),
wrap the execution.output.collect and execution.await call in a try-catch that
handles CancellationException and other exceptions, and before calling
activity.appendBuildOutput(...) or resetProgressIfIdle(activity) check
activity.isFinishing/isDestroyed (or use lifecycleScope which cancels
automatically) so UI updates are only attempted when the Activity is alive; keep
manager.notifyActionCompleted(pluginId, actionId, result) behavior but ensure it
runs in a safe context if it touches UI.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt`:
- Around line 81-96: The executeAction function can throw from
commandService.executeCommand after extension?.onActionStarted(actionId) was
called, leaving plugins without onActionCompleted and activeExecutions
inconsistent; wrap the call to commandService.executeCommand(action.command,
action.timeoutMs) in a try/catch, on success continue to store the execution in
activeExecutions as now, but on catch call
extension?.onActionCompleted(actionId) (and ensure nothing was added to
activeExecutions) and either rethrow the exception or return null so the caller
knows it failed; ensure the executionKey and activeExecutions[executionKey]
assignment remain only after a successful executeCommand.
- Around line 50-55: The current code in PluginBuildActionManager silently
swallows Throwable when iterating extension.getBuildActions() and at the other
two similar sites, which hides serious errors; change each broad catch (_:
Throwable) to catch a narrow, expected exception type (e.g.,
IllegalStateException or Exception) and log the error instead of ignoring it —
include pluginId and name (and action when available) in the log message to aid
debugging; apply the same change for the other two occurrences in the same class
(the blocks that create RegisteredBuildAction entries) so failures are visible
and do not suppress Errors like OutOfMemoryError.
- Around line 115-120: In cleanupPlugin(pluginId: String) first find all
matching entries in activeExecutions whose keys startWith("$pluginId:"), call
the appropriate cancellation/termination method on each execution handle (e.g.,
cancel()/terminate()/stop() depending on the execution handle API) to stop the
running processes, wait or handle failures if the API is asynchronous, and only
after canceling remove those entries from activeExecutions; then proceed to
remove pluginExtensions.remove(pluginId), manifestActions.remove(pluginId), and
pluginNames.remove(pluginId).

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt`:
- Around line 20-22: The catch-all `catch (_: Throwable)` in
PluginDrawableResolver.kt should be replaced with narrow, explicit catches for
known recoverable exceptions (e.g., `Resources.NotFoundException` is already
caught; add `IllegalArgumentException` or other specific exceptions you've
observed) and avoid swallowing `Error` types; for any unexpected exception types
that you do want to catch, add a minimal log (e.g., warning with the exception
message and context) so failures are visible instead of silent. Locate the
try/catch block in class PluginDrawableResolver and replace the broad Throwable
catch with explicit exception types relevant to drawable resolution (and add
logging in those unexpected-but-handled branches).

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt`:
- Around line 135-140: resetProgressIfIdle currently only checks
buildService?.isBuildInProgress which is never updated by plugin actions; either
update the plugin action lifecycle to flip buildService.isBuildInProgress when a
plugin build starts/ends (set buildService?.isBuildInProgress = true at action
start and false at completion in the PluginBuildActionItem action handlers) or
add a separate flag (e.g.,
EditorHandlerActivity.editorViewModel.isPluginActionInProgress) and change
resetProgressIfIdle to consider both buildService?.isBuildInProgress and
isPluginActionInProgress so UI progress reflects both Gradle builds and plugin
actions consistently.

In `@app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt`:
- Around line 192-193: The empty catch block swallowing all Exceptions should be
replaced: catch only the expected exception types thrown during plugin
registration (e.g., IllegalStateException/IOException or the concrete exceptions
your plugin APIs throw) or, if you must catch Exception, log the error instead
of ignoring it; update the catch in EditorActivityActions.kt (the catch (_:
Exception) { } block used around plugin registration) to call the project
logging utility (or Android Log.e/Timber) with a clear message like "plugin
registration failed" and include the exception so failures are visible for
debugging.

In `@plugin-api/build.gradle.kts`:
- Around line 32-33: Keep the api configuration but update the
kotlinx-coroutines dependency version: replace the
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") entry with a 1.10.0+
version (e.g., 1.10.0 or later) to follow Kotlin 2.1.21 recommendations and
ensure compatibility for the public Flow<CommandOutput> in IdeCommandService.

In
`@plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeCommandService.kt`:
- Around line 8-20: Add KDoc comments describing the public API surface:
document the IdeCommandService interface and each method (executeCommand,
isCommandRunning, cancelCommand, getRunningCommandCount) and the
CommandExecution interface and its members (executionId, output, await, cancel)
to explain purpose, parameters, return types, default values (e.g., timeoutMs
default 600_000), possible exceptions/behaviour (cancellation semantics,
lifecycle of output Flow), and usage examples or thread-safety notes as
appropriate to help plugin authors discover and use these APIs.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt`:
- Around line 61-75: ManifestBuildAction has mixed use of `@SerializedName` which
can confuse the expected JSON schema; update the data class by adding
`@SerializedName` annotations for all properties (id, name, description, category,
command, arguments, environment) to match the JSON field names you expect (e.g.,
"id", "name", "description", "category", "command", "arguments", "environment")
while retaining the existing annotations for gradleTask ("gradle_task"),
workingDirectory ("working_directory") and timeoutMs ("timeout_ms") so
serialization is consistent and explicit across the class.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt`:
- Around line 50-56: CommandSpec.GradleTask currently assumes gradlew exists and
is executable; modify the block that builds gradleWrapper so you explicitly
validate File(projectRoot, "gradlew") exists and isFile and isExecutable (or
attempt gradleWrapper.setExecutable(true) and re-check), and if those checks
fail throw a clear IllegalStateException mentioning missing or non-executable
gradlew; then proceed to construct the ProcessBuilder with
gradleWrapper.absolutePath, spec.taskPath and spec.arguments as before. Ensure
you reference gradleWrapper, projectRoot, CommandSpec.GradleTask and
ProcessBuilder when locating and updating the code.
- Around line 85-88: cancelAllCommands has a race where a command can be added
between iterating and clear(); fix by making the cancel-and-clear atomic: wrap
the loop and clear in a single synchronized block (or use a dedicated mutex) so
that runningCommands.values.forEach { it.cancel() } and runningCommands.clear()
execute under the same lock; update the cancelAllCommands implementation to
synchronize on the runningCommands instance (or the class-level lock) to prevent
new entries being added while cancelling.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt`:
- Around line 18-19: In PluginDrawableResolver (the method that currently does
"val drawable = ContextCompat.getDrawable(pluginContext, resId); return
drawable"), remove the unnecessary local variable and return the result of
ContextCompat.getDrawable(pluginContext, resId) directly to simplify the code
and improve conciseness; update the method body to a single direct return of
ContextCompat.getDrawable(pluginContext, resId).
🪄 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: 0c86fe83-7c35-439f-b672-371e07a39546

📥 Commits

Reviewing files that changed from the base of the PR and between cb05a7d and e64ffea.

📒 Files selected for processing (12)
  • app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt
  • app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt
  • app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt
  • app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt
  • plugin-api/build.gradle.kts
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/BuildActionExtension.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeCommandService.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeCommandServiceImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt

Comment on lines +42 to +62
override fun prepare(data: ActionData) {
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true

val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)

if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}
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.

⚠️ Potential issue | 🟡 Minor

Missing super.prepare(data) call.

BaseBuildAction.prepare() performs context validation and sets visible/enabled based on build service state. Not calling super.prepare(data) bypasses these checks. While you're handling visibility manually, the base class behavior should be considered.

🔧 Proposed fix
     override fun prepare(data: ActionData) {
+        super.prepare(data)
         val context = data.getActivity()
         if (context == null) {
             visible = false
             return
         }
         visible = true

Note: If you intentionally want to bypass BaseBuildAction's buildService.isBuildInProgress check (since plugin actions have their own running state), consider documenting this or extracting a different base class.

📝 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 prepare(data: ActionData) {
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true
val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)
if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}
override fun prepare(data: ActionData) {
super.prepare(data)
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true
val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)
if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt`
around lines 42 - 62, The override of prepare in PluginBuildActionItem is
missing a call to the base implementation; call super.prepare(data) at the start
of PluginBuildActionItem.prepare so BaseBuildAction.prepare can perform its
context validation and set visible/enabled based on buildService state before
you apply plugin-specific logic (or if you intentionally want to bypass that
behavior, add a comment explaining the reason and consider extracting a
different base class rather than skipping super.prepare); keep the rest of the
method (using PluginBuildActionManager.isActionRunning and setting
label/icon/enabled) unchanged.

Comment on lines +112 to +130
actionScope.launch {
execution.output.collect { output ->
val line = when (output) {
is CommandOutput.StdOut -> output.line
is CommandOutput.StdErr -> output.line
is CommandOutput.ExitCode ->
if (output.code != 0) "Process failed with code ${output.code}" else null
}
if (line != null) {
withContext(Dispatchers.Main) {
activity.appendBuildOutput(line)
}
}
}

val result = execution.await()
manager.notifyActionCompleted(pluginId, actionId, result)
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}
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.

⚠️ Potential issue | 🟡 Minor

Consider lifecycle-safe coroutine handling.

The coroutine launched in actionScope may outlive the activity if the user navigates away during a long-running build. This could lead to crashes when withContext(Dispatchers.Main) tries to update a destroyed activity. Consider:

  1. Using activity.lifecycleScope instead of actionScope for automatic cancellation.
  2. Adding null-safety checks before UI updates.
  3. Wrapping the collect/await in try-catch for cancellation exceptions.
🛡️ Proposed lifecycle-safe pattern
-        actionScope.launch {
-            execution.output.collect { output ->
+        activity.lifecycleScope.launch {
+            try {
+                execution.output.collect { output ->
                 val line = when (output) {
                     is CommandOutput.StdOut -> output.line
                     is CommandOutput.StdErr -> output.line
                     is CommandOutput.ExitCode ->
                         if (output.code != 0) "Process failed with code ${output.code}" else null
                 }
                 if (line != null) {
-                    withContext(Dispatchers.Main) {
-                        activity.appendBuildOutput(line)
-                    }
+                    activity.appendBuildOutput(line)
                 }
-            }
+                }

-            val result = execution.await()
-            manager.notifyActionCompleted(pluginId, actionId, result)
-            withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
+                val result = execution.await()
+                manager.notifyActionCompleted(pluginId, actionId, result)
+                resetProgressIfIdle(activity)
+            } catch (e: CancellationException) {
+                manager.cancelAction(pluginId, actionId)
+                throw e
+            }
         }

Note: Using lifecycleScope automatically runs on Main dispatcher and cancels when the activity is destroyed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/actions/build/PluginBuildActionItem.kt`
around lines 112 - 130, The coroutine launched on actionScope can outlive the
Activity and call withContext(Dispatchers.Main) after the Activity is destroyed;
change the launch to use the Activity lifecycle (replace actionScope.launch with
activity.lifecycleScope.launch or otherwise tie the Job to activity.lifecycle),
wrap the execution.output.collect and execution.await call in a try-catch that
handles CancellationException and other exceptions, and before calling
activity.appendBuildOutput(...) or resetProgressIfIdle(activity) check
activity.isFinishing/isDestroyed (or use lifecycleScope which cancels
automatically) so UI updates are only attempted when the Activity is alive; keep
manager.notifyActionCompleted(pluginId, actionId, result) behavior but ensure it
runs in a safe context if it touches UI.

Comment on lines +50 to +55
try {
extension.getBuildActions().forEach { action ->
actions.add(RegisteredBuildAction(pluginId, name, action))
}
} catch (_: Throwable) {}
}
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.

⚠️ Potential issue | 🟠 Major

Swallowing Throwable hides critical errors and impedes debugging.

Catching and silently ignoring Throwable (which includes Error types like OutOfMemoryError) violates fail-fast principles and makes plugin failures invisible. This pattern repeats at lines 72-75 and 124-126.

Based on learnings, prefer narrow exception handling that catches only specific expected exceptions (e.g., IllegalStateException) rather than a broad catch-all.

🛠️ Proposed fix: Log errors and narrow the exception type
             try {
                 extension.getBuildActions().forEach { action ->
                     actions.add(RegisteredBuildAction(pluginId, name, action))
                 }
-            } catch (_: Throwable) {}
+            } catch (e: Exception) {
+                // Log plugin failure for debugging; consider surfacing to user
+                android.util.Log.w("PluginBuildActionManager", "Failed to get build actions from plugin $pluginId", e)
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt`
around lines 50 - 55, The current code in PluginBuildActionManager silently
swallows Throwable when iterating extension.getBuildActions() and at the other
two similar sites, which hides serious errors; change each broad catch (_:
Throwable) to catch a narrow, expected exception type (e.g.,
IllegalStateException or Exception) and log the error instead of ignoring it —
include pluginId and name (and action when available) in the log message to aid
debugging; apply the same change for the other two occurrences in the same class
(the blocks that create RegisteredBuildAction entries) so failures are visible
and do not suppress Errors like OutOfMemoryError.

Comment on lines +81 to +96
fun executeAction(
pluginId: String,
actionId: String,
commandService: IdeCommandService
): CommandExecution? {
val action = findAction(pluginId, actionId) ?: return null
val extension = pluginExtensions[pluginId]

extension?.onActionStarted(actionId)

val execution = commandService.executeCommand(action.command, action.timeoutMs)
val executionKey = "$pluginId:$actionId"
activeExecutions[executionKey] = execution

return execution
}
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.

⚠️ Potential issue | 🟡 Minor

Missing error handling leaves actions in inconsistent state if executeCommand throws.

If commandService.executeCommand() throws an exception after onActionStarted() is called, the plugin never receives onActionCompleted(), leaving it in an inconsistent state.

🛠️ Proposed fix
 fun executeAction(
     pluginId: String,
     actionId: String,
     commandService: IdeCommandService
 ): CommandExecution? {
     val action = findAction(pluginId, actionId) ?: return null
     val extension = pluginExtensions[pluginId]

     extension?.onActionStarted(actionId)

-    val execution = commandService.executeCommand(action.command, action.timeoutMs)
-    val executionKey = "$pluginId:$actionId"
-    activeExecutions[executionKey] = execution
-
-    return execution
+    return try {
+        val execution = commandService.executeCommand(action.command, action.timeoutMs)
+        val executionKey = "$pluginId:$actionId"
+        activeExecutions[executionKey] = execution
+        execution
+    } catch (e: Exception) {
+        extension?.onActionCompleted(actionId, CommandResult.Failure(-1, "", "", e.message, 0))
+        throw e
+    }
 }
📝 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
fun executeAction(
pluginId: String,
actionId: String,
commandService: IdeCommandService
): CommandExecution? {
val action = findAction(pluginId, actionId) ?: return null
val extension = pluginExtensions[pluginId]
extension?.onActionStarted(actionId)
val execution = commandService.executeCommand(action.command, action.timeoutMs)
val executionKey = "$pluginId:$actionId"
activeExecutions[executionKey] = execution
return execution
}
fun executeAction(
pluginId: String,
actionId: String,
commandService: IdeCommandService
): CommandExecution? {
val action = findAction(pluginId, actionId) ?: return null
val extension = pluginExtensions[pluginId]
extension?.onActionStarted(actionId)
return try {
val execution = commandService.executeCommand(action.command, action.timeoutMs)
val executionKey = "$pluginId:$actionId"
activeExecutions[executionKey] = execution
execution
} catch (e: Exception) {
extension?.onActionCompleted(actionId, CommandResult.Failure(-1, "", "", e.message, 0))
throw e
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt`
around lines 81 - 96, The executeAction function can throw from
commandService.executeCommand after extension?.onActionStarted(actionId) was
called, leaving plugins without onActionCompleted and activeExecutions
inconsistent; wrap the call to commandService.executeCommand(action.command,
action.timeoutMs) in a try/catch, on success continue to store the execution in
activeExecutions as now, but on catch call
extension?.onActionCompleted(actionId) (and ensure nothing was added to
activeExecutions) and either rethrow the exception or return null so the caller
knows it failed; ensure the executionKey and activeExecutions[executionKey]
assignment remain only after a successful executeCommand.

Comment on lines +115 to +120
fun cleanupPlugin(pluginId: String) {
activeExecutions.entries.removeAll { it.key.startsWith("$pluginId:") }
pluginExtensions.remove(pluginId)
manifestActions.remove(pluginId)
pluginNames.remove(pluginId)
}
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.

⚠️ Potential issue | 🟠 Major

cleanupPlugin should cancel active executions before removing them.

Simply removing entries from activeExecutions leaves processes running in the background without any way to manage them afterward. This can cause resource leaks and orphaned processes.

🛠️ Proposed fix
 fun cleanupPlugin(pluginId: String) {
-    activeExecutions.entries.removeAll { it.key.startsWith("$pluginId:") }
+    activeExecutions.entries
+        .filter { it.key.startsWith("$pluginId:") }
+        .forEach { (key, execution) ->
+            execution.cancel()
+            activeExecutions.remove(key)
+        }
     pluginExtensions.remove(pluginId)
     manifestActions.remove(pluginId)
     pluginNames.remove(pluginId)
 }
📝 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
fun cleanupPlugin(pluginId: String) {
activeExecutions.entries.removeAll { it.key.startsWith("$pluginId:") }
pluginExtensions.remove(pluginId)
manifestActions.remove(pluginId)
pluginNames.remove(pluginId)
}
fun cleanupPlugin(pluginId: String) {
activeExecutions.entries
.filter { it.key.startsWith("$pluginId:") }
.forEach { (key, execution) ->
execution.cancel()
activeExecutions.remove(key)
}
pluginExtensions.remove(pluginId)
manifestActions.remove(pluginId)
pluginNames.remove(pluginId)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/build/PluginBuildActionManager.kt`
around lines 115 - 120, In cleanupPlugin(pluginId: String) first find all
matching entries in activeExecutions whose keys startWith("$pluginId:"), call
the appropriate cancellation/termination method on each execution handle (e.g.,
cancel()/terminate()/stop() depending on the execution handle API) to stop the
running processes, wait or handle failures if the API is asynchronous, and only
after canceling remove those entries from activeExecutions; then proceed to
remove pluginExtensions.remove(pluginId), manifestActions.remove(pluginId), and
pluginNames.remove(pluginId).

Comment on lines +186 to +201
} catch (e: kotlinx.coroutines.TimeoutCancellationException) {
process?.destroyForcibly()
outputChannel.close()
val duration = System.currentTimeMillis() - startTime
resultDeferred.complete(
CommandResult.Failure(-1, stdoutBuilder.toString(), stderrBuilder.toString(), "Command timed out after ${timeoutMs}ms", duration)
)
} catch (e: Exception) {
process?.destroyForcibly()
outputChannel.close()
val duration = System.currentTimeMillis() - startTime
if (resultDeferred.isActive) {
resultDeferred.complete(
CommandResult.Cancelled(stdoutBuilder.toString(), stderrBuilder.toString())
)
}
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.

⚠️ Potential issue | 🟠 Major

Swallowed exceptions lose diagnostic information.

Both exception handlers discard the exception details. For TimeoutCancellationException (line 186), the message is reconstructed but the original exception is lost. For generic Exception (line 193), no error information is preserved in the Cancelled result, making debugging difficult.

Per retrieved learnings and static analysis hints, prefer narrow exception handling and preserve error context.

🛠️ Proposed fix: Preserve exception information and narrow catch type
             } catch (e: kotlinx.coroutines.TimeoutCancellationException) {
                 process?.destroyForcibly()
                 outputChannel.close()
                 val duration = System.currentTimeMillis() - startTime
                 resultDeferred.complete(
-                    CommandResult.Failure(-1, stdoutBuilder.toString(), stderrBuilder.toString(), "Command timed out after ${timeoutMs}ms", duration)
+                    CommandResult.Failure(-1, stdoutBuilder.toString(), stderrBuilder.toString(), "Command timed out after ${timeoutMs}ms: ${e.message}", duration)
                 )
-            } catch (e: Exception) {
+            } catch (e: kotlinx.coroutines.CancellationException) {
                 process?.destroyForcibly()
                 outputChannel.close()
-                val duration = System.currentTimeMillis() - startTime
                 if (resultDeferred.isActive) {
                     resultDeferred.complete(
                         CommandResult.Cancelled(stdoutBuilder.toString(), stderrBuilder.toString())
                     )
                 }
+            } catch (e: Exception) {
+                process?.destroyForcibly()
+                outputChannel.close()
+                val duration = System.currentTimeMillis() - startTime
+                if (resultDeferred.isActive) {
+                    resultDeferred.complete(
+                        CommandResult.Failure(-1, stdoutBuilder.toString(), stderrBuilder.toString(), "Unexpected error: ${e.message}", duration)
+                    )
+                }
             } finally {
🧰 Tools
🪛 detekt (1.23.8)

[warning] 186-186: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)


[warning] 193-193: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

Comment on lines +20 to +22
} catch (_: Resources.NotFoundException) {
} catch (_: Throwable) {
}
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.

⚠️ Potential issue | 🟠 Major

Overly broad exception handling: prefer narrow catch clauses.

Catching Throwable silently swallows Error types (e.g., OutOfMemoryError, StackOverflowError) that should propagate, and makes debugging difficult due to the empty catch body. Consider catching only specific exceptions that are known to occur here.

If additional exception types beyond Resources.NotFoundException have been observed, catch those explicitly and consider adding minimal logging for unexpected cases.

🛡️ Proposed fix: Narrow the exception handling
             try {
                 val drawable = ContextCompat.getDrawable(pluginContext, resId)
                 return drawable
             } catch (_: Resources.NotFoundException) {
-            } catch (_: Throwable) {
+                // Drawable not found in plugin context; fall through to fallback
             }

If a specific additional exception has been identified (e.g., IllegalArgumentException), add it explicitly:

} catch (_: Resources.NotFoundException) {
} catch (_: IllegalArgumentException) {
}

Based on learnings: "In Kotlin files across the AndroidIDE project, prefer narrow exception handling that catches only the specific exception type reported in crashes instead of a broad catch-all. This aligns with fail-fast behavior during development."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt`
around lines 20 - 22, The catch-all `catch (_: Throwable)` in
PluginDrawableResolver.kt should be replaced with narrow, explicit catches for
known recoverable exceptions (e.g., `Resources.NotFoundException` is already
caught; add `IllegalArgumentException` or other specific exceptions you've
observed) and avoid swallowing `Error` types; for any unexpected exception types
that you do want to catch, add a minimal log (e.g., warning with the exception
message and context) so failures are visible instead of silent. Locate the
try/catch block in class PluginDrawableResolver and replace the broad Throwable
catch with explicit exception types relevant to drawable resolution (and add
logging in those unexpected-but-handled branches).

@hasanelfalakiy
Copy link
Copy Markdown

hasanelfalakiy commented Apr 3, 2026

is it possible to run .java and .kt files like in Intellij Idea? the results of which appear on the console

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