Skip to content

ADFA-4928 create a single manager for plugins and templates - #1627

Open
hal-eisen-adfa wants to merge 19 commits into
stagefrom
ADFA-4928-Create-a-single-manager-for-plugins-and-templates
Open

ADFA-4928 create a single manager for plugins and templates#1627
hal-eisen-adfa wants to merge 19 commits into
stagefrom
ADFA-4928-Create-a-single-manager-for-plugins-and-templates

Conversation

@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator

We currently don't have any UI for adding a template to CoGo. It's a very similar idea to adding a plugin, so let's try putting them together.

yaturner and others added 9 commits July 29, 2026 22:26
Adds the Compose plugin/buildFeatures/dependencies to app/build.gradle.kts,
mirroring the floating-window/profiler modules' setup, plus a shared
ManagerTheme composable that resolves Theme.AndroidIDE's Material3 attrs
(same technique as FloatingTheme). This is the first commit of the
Plugin Manager + Template Manager merge (ADR 0009 requires new screens
to be Compose); the theme/build wiring lands separately from any
screen code so it's independently reviewable and buildable.
Rebuilds PluginManagerActivity's screen in Jetpack Compose (ADR 0009),
preserving every capability of the old RecyclerView/dialogs UI:
install via SAF picker, enable/disable/uninstall, overwrite and
signature-mismatch conflict handling, restart prompt, and the
discover-plugins action. PluginManagerViewModel/PluginRepository are
reused unchanged.

The six long-press tooltip anchor points collapse to two (list items,
and the screen's background/empty state) since they all showed the
same TooltipTag.PLUGIN_MANAGER content anyway - verified on-device
that the long-press still correctly reaches TooltipManager.

Also moves two dialogs' hardcoded English strings (uninstall
confirmation, plugin details labels) into string resources.

Note: taken together with the prior commit, this is the buildable/
tested state; the prior commit's PluginListAdapter.kt deletion was
accidentally bundled with the build-wiring commit rather than this
one, so that earlier commit alone doesn't compile in isolation - only
the combined history does (verified via :app:assembleV8Debug and a
manual on-device pass).
Ports the parsing/model layer from appdevforall/TemplateManagerPlugin
(CgtTemplateReader, TemplateMetadata/CgtFileItem, plus their unit tests)
into the app module as the basis for the new Templates tab.

Adds TemplateRepository/TemplateRepositoryImpl, which reimplement the
plugin's install/uninstall/delete semantics as direct file operations
on Environment.TEMPLATES_DIR + the Downloads folder, since the host app
doesn't need IdeTemplateService's plugin-facing permission gate.
Provenance (bundled/plugin/user) is inferred from the same filename
convention IdeTemplateServiceImpl/PluginProjectManager already use.

Adds TemplateManagerViewModel (UDF shape matching PluginManagerViewModel)
and a Koin di/TemplateModule, registered in IDEApplication alongside
pluginModule. No UI yet - this commit is data-layer only.

CgtTemplateReaderTest needs @RunWith(RobolectricTestRunner::class):
org.json.JSONObject throws "not mocked" under a plain JVM unit test,
same as other app-module tests that touch real android.jar classes.
Adds the Compose UI for the Templates tab, backed by the data layer
from the previous commit: TemplateListItem (card - tapping only opens
the multi-template sub-list, matching the reference plugin's design),
TemplateManagerDialogs (delete confirmation, file-level details,
per-template details, multi-template sub-list), and TemplateManagerScreen
(content composable wiring the ViewModel's uiState/uiEffect, same
long-press pointerInput tooltip shim as the Plugins tab, new
TooltipTag.TEMPLATE_MANAGER).

TemplateManagerScreen is content-only (no Scaffold/TopAppBar/FAB) -
unlike the Plugins tab there's no install-flow FAB, matching the
ported plugin's passive Downloads-folder scanning. It's meant to be
composed as one tab's body inside the shared manager screen; wiring
the two tabs together is the next commit.
New ManagerScreen composable owns the shared Scaffold/TopAppBar/TabRow
+ HorizontalPager, hosting Plugins and Templates as pages (Plugins
default). The FAB and discover-plugins action only render on the
Plugins tab, since Templates is a passive Downloads-folder scan with
no equivalent action.

Refactors the old PluginManagerScreen into PluginManagerContent - a
Scaffold-free content composable, matching TemplateManagerScreen's
shape - so both tabs plug into ManagerScreen's single Scaffold instead
of nesting their own. PluginManagerActivity now resolves both
PluginManagerViewModel and TemplateManagerViewModel and renders
ManagerScreen; its class name and entry points (Settings, the
crash-recovery dialog) are unchanged.

Updates ARCHITECTURE.md: this is the first production Compose screen
in app (ADR 0009), and templates/manager is a new data-layer package.

Verified end-to-end on a physical device: assembleV8Debug, installed
APK, exercised both tabs from Settings -> Plugin Manager. Templates
tab correctly scanned Environment.TEMPLATES_DIR + Downloads (found
real pre-existing .cgt fixtures on the test device), and a full
install/uninstall round-trip moved files between Downloads and
TEMPLATES_DIR and refreshed the list correctly. No crashes.
…ity + docs)

Finishes the previous commit: a staging mistake (a `git add` call hit a
stale pathspec and aborted before reaching these files) left
`81e3797ab` with only the new `ManagerScreen.kt` and a content-less
file rename, referencing a `PluginManagerContent` composable that
didn't exist yet in that commit alone - not independently buildable.

This commit adds what was missed: the actual `PluginManagerContent.kt`
refactor (Scaffold/TopAppBar/FAB stripped out, now content-only),
`PluginManagerActivity.kt` wired to render `ManagerScreen` with both
view models, the `ARCHITECTURE.md` updates, and the `title_manager`
string. Combined history through this commit compiles
(:app:compileV8DebugKotlin) and matches what was already verified
end-to-end on-device in the previous message.
The Settings entry that opens the merged Plugins/Templates screen
was still titled "Plugin Manager" with a summary mentioning
"extensions" (the old plugin-only wording). Renamed to
"Extensions Manager" with a summary reflecting both tabs it now
opens: "Manage IDE plugins and templates".

Verified on-device: preferences list and the opened screen both
render correctly.
PluginModule's Koin factories called Context.filesDir directly, which
does a real File.exists() check on every call, not just the first.
That trips StrictMode's DiskReadViolation the first time the Extensions
Manager screen resolves PluginRepository/PluginManagerViewModel on the
main thread.

Cache the resolved File once, off-main, during app startup
(IDEApplication.cachedFilesDir), and have PluginModule read that
instead - later reads are then a plain field access rather than a
syscall.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The SAF picker launched with "*/*", showing every file regardless of
type. SAF filters by MIME, not extension, and .cgp has no registered
MIME type, so the closest working filter is "application/octet-stream" -
what document providers report for files with an unrecognized
extension. This hides files with a known type (zips, jars, images,
...)  while leaving .cgp files selectable. isSupportedPluginFile()
still validates the actual pick, since this is an approximation, not
an exact extension filter (SAF has no such thing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@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 for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

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

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@jimturner-adfa, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c14e549d-70b3-4b93-a207-5ee350dce904

📥 Commits

Reviewing files that changed from the base of the PR and between 9d124ac and 2b7a636.

📒 Files selected for processing (1)
  • app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt
📝 Walkthrough
  • Added a unified Compose-based Extensions Manager with Plugins and Templates tabs.
  • Preserved plugin discovery, installation, enable/disable, uninstall, conflict handling, restart prompts, and error feedback.
  • Added .cgt template parsing, metadata models, provenance tracking, installation, uninstallation, deletion, and detail dialogs.
  • Added template repositories, ViewModel state management, Koin dependency injection, and background file operations.
  • Added bitmap downsampling, buffered plugin effects, improved URI error handling, and Direct Boot-safe cachedFilesDir initialization.
  • Updated settings terminology, architecture documentation, and plugin authoring documentation.
  • Added parser and model tests.
  • Risk: Long-press tooltip handling may still trigger the associated button action on release.
  • Risk: The combined manager increases UI and lifecycle complexity.
  • Risk: File installation and deletion modify user files. Conflict handling and validation reduce, but do not remove, data-loss risk.
  • Risk: Direct Boot and unavailable credential-protected storage require validation on devices that start before unlock.
  • Risk: Compose effect delivery, state restoration, accessibility semantics, loading states, and lifecycle collection require regression testing during tab changes and activity recreation.
  • Risk: Repository, ViewModel, and UI test coverage remains limited.

Walkthrough

The Extensions Manager replaces the legacy plugin UI with Compose. It adds template parsing, storage operations, UDF state, ViewModels, dialogs, tabs, theming, dependency injection, file validation, and tests.

Changes

Extensions manager

Layer / File(s) Summary
Compose and dependency wiring
app/build.gradle.kts, gradle/libs.versions.toml, app/src/main/java/com/itsaky/androidide/app/..., app/src/main/java/com/itsaky/androidide/di/..., ARCHITECTURE.md, resources/src/main/res/values/strings.xml, idetooltips/..., common/..., docs/PLUGIN_AUTHORING.md
The app enables Compose, configures mixed JUnit execution, caches filesDir, registers template dependencies, warms storage after credential unlock, updates localized resources, and documents the architecture.
Template models, parsing, and repository
app/src/main/java/com/itsaky/androidide/templates/manager/..., app/src/main/java/com/itsaky/androidide/repositories/..., app/src/test/java/com/itsaky/androidide/templates/manager/...
The change adds .cgt models, ZIP parsing, provenance tracking, template discovery, file operations, and model/parser tests.
Template UDF state and Compose UI
app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt, app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt, app/src/main/java/com/itsaky/androidide/ui/compose/templates/..., resources/src/main/res/values/strings.xml
The template manager adds state, events, effects, asynchronous operations, list items, empty states, dialogs, template selection, and localized feedback.
Plugin Compose migration
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/..., app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt, app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt, common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
Plugin rendering, dialogs, file validation, image loading, and feedback move into Compose components. Selected plugin files now use asynchronous .cgp validation.
Shared manager shell and theme
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt, app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt, app/src/main/java/com/itsaky/androidide/ui/compose/theme/ManagerTheme.kt, app/src/main/res/layout/activity_plugin_manager.xml, idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt
The activity hosts a themed Compose manager with plugin and template tabs. The XML layout now contains a full-screen ComposeView.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PluginManagerActivity
  participant ManagerScreen
  participant TemplateManagerScreen
  participant TemplateManagerViewModel
  participant TemplateRepository
  PluginManagerActivity->>ManagerScreen: Render manager tabs
  ManagerScreen->>TemplateManagerScreen: Show Templates tab
  TemplateManagerScreen->>TemplateManagerViewModel: Dispatch template event
  TemplateManagerViewModel->>TemplateRepository: Load or mutate template files
  TemplateRepository-->>TemplateManagerViewModel: Return Result
  TemplateManagerViewModel-->>TemplateManagerScreen: Emit state and effects
Loading

Possibly related PRs

Suggested reviewers: jatezzz, jomen-adfa

Poem

A rabbit checks the Compose screen,
Plugins and templates now convene.
CGT files parse with care,
Koin wires them everywhere.
Tabs and dialogs guide each feat—
The manager is carrot-sweet.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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: creating a unified manager for plugins and templates.
Description check ✅ Passed The description directly explains the motivation for combining template and plugin management in one UI.
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 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch ADFA-4928-Create-a-single-manager-for-plugins-and-templates
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-4928-Create-a-single-manager-for-plugins-and-templates

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

🧹 Nitpick comments (3)
app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt (1)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the project logger facade.

android.util.Log and interpolated messages bypass the required structured logging contract. Replace TAG with LoggerFactory and use placeholders for dynamic values.

As per coding guidelines, use SLF4J LoggerFactory with structured placeholders.

Also applies to: 55-61, 77-82, 97-102, 124-129

🤖 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/viewmodels/TemplateManagerViewModel.kt`
at line 3, Replace android.util.Log and the TAG-based logging in
TemplateManagerViewModel with the project’s SLF4J LoggerFactory facade. Update
all affected logging calls, including the referenced ranges, to use structured
placeholder arguments instead of interpolated messages, and remove the obsolete
TAG declaration/import.

Source: Coding guidelines

app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt (1)

6-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add KDoc for the new public manager APIs.

The new public types and composables lack contract documentation. Document state ownership, effect delivery, destructive-action behavior, and caller expectations.

  • app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt#L6-L76: add KDoc for the state, event, effect, and operation contracts.
  • app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt#L40-L49: document event dispatch behavior and threading expectations.
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt#L45-L55: document plugin action and tooltip callback contracts.
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt#L25-L123: document each dialog confirmation and dismissal contract.

As per coding guidelines, public classes and functions require KDoc or Javadoc.

🤖 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/ui/models/TemplateManagerUiState.kt`
around lines 6 - 76, Add KDoc for the public contracts in
app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt:6-76,
covering TemplateManagerUiState, TemplateManagerUiEvent,
TemplateManagerUiEffect, and TemplateOperation, including state ownership,
effect delivery, destructive actions, and caller expectations. Document event
dispatch behavior and threading expectations for the relevant API in
app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt:40-49.
Document plugin action and tooltip callback contracts in
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt:45-55,
and add KDoc for each dialog’s confirmation and dismissal contract in
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt:25-123.

Source: Coding guidelines

app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt (1)

89-109: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated error-flashbar presentation in both tab bodies. Both tab contents build the same error flashbar: the 5000L versus DURATION_INDEFINITE duration heuristic, the error icon, the message, the conditional copy action with a clipboard write, and showOnUiThread(). Only the clip label resource differs. The shared root cause is one missing helper.

  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt#L89-L109: replace this block with a call to a shared helper, for example ComponentActivity.showEffectError(messageResId, formatArgs, R.string.msg_template_error_clip_label), and define the duration as a named constant.
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt#L133-L153: replace this block with the same helper, passing R.string.msg_plugin_error_clip_label.

Reuse existing helpers, extract duplicated logic, replace repeated magic values with named constants, as required by the coding guidelines.

🤖 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/ui/compose/templates/TemplateManagerScreen.kt`
around lines 89 - 109, The error flashbar presentation is duplicated across both
tab bodies. In
app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt:89-109
and
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt:133-153,
extract the shared logic into a ComponentActivity helper that accepts the
message resource, format arguments, and clip-label resource; replace both blocks
with calls to it, using the template and plugin clip labels respectively. Define
the 5000L duration as a named constant and preserve the conditional copy action
and indefinite duration behavior.

Source: Coding guidelines

🤖 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/PluginManagerActivity.kt`:
- Around line 39-47: Update ManagerScreen’s Scaffold to use WindowInsets(0) for
contentWindowInsets, since binding.root already applies system-bar padding; keep
the activity’s existing root padding and prevent duplicate inset spacing around
the tab row, pager, and FAB.

In `@app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt`:
- Around line 147-155: Ensure IDEApplication.cachedFilesDir is initialized off
the main thread before Koin can resolve PluginManagerViewModel: update
IDEApplication.cachedFilesDir and the warmup in
DeviceProtectedApplicationLoader.load() so initialization completes before
ensureKoinStarted() exposes pluginModule, and verify PluginModule uses the
already-initialized cache without triggering lazy initialization on the main
thread. Apply the required changes in
app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt (lines 147-155),
app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt
(lines 137-145), and app/src/main/java/com/itsaky/androidide/di/PluginModule.kt
(lines 19-32).

In
`@app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt`:
- Around line 81-84: Update the template installation logic around the file-copy
operations in TemplateRepositoryImpl so the result of item.file.delete() is
validated. If source deletion fails, remove the newly created dest copy and
return the operation’s defined failure/recovery result instead of reloading
providers or reporting success; apply the same handling to both affected
methods.
- Line 3: Replace android.util.Log usage throughout TemplateRepositoryImpl with
an SLF4J logger created via LoggerFactory. Update the referenced logging calls
to use appropriate SLF4J levels and structured `{}` placeholders with arguments
instead of string concatenation or interpolation.
- Around line 77-85: Update installTemplate and the corresponding
uninstallTemplate flow to detect an existing destination before copying and
refuse the operation unless an explicit user-confirmed replacement is provided.
Remove the unconditional overwrite behavior in File.copyTo, preserving
bundled-provenance protection and preventing unrelated same-name archives from
being replaced.
- Around line 32-36: Replace the broad runCatching usage in listTemplateFiles
and the other indicated repository I/O paths with explicit exception handling:
catch only expected file, parsing, and provider exceptions, rethrow
CancellationException, and handle unexpected failures explicitly rather than
using onFailure solely to log them. Preserve each method’s existing Result
success/failure contract and logging context.

In
`@app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt`:
- Around line 5-11: Document the public model contracts with KDoc for
TemplateMetadata and CgtFileItem. Describe each model’s purpose, clarify the
semantics of installed and provenance, and explain how a single archive can
contain multiple templates; retain the existing optionalTags field documentation
and add property-level KDoc where needed for these non-obvious meanings.

In `@app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt`:
- Around line 35-37: Update the image-loading logic in FileImage to read source
bounds first, calculate an inSampleSize that limits the decoded bitmap to the
40.dp icon’s required dimensions, and decode using those options before
converting with asImageBitmap. Replace broad runCatching with targeted
recoverable-failure handling, while allowing CancellationException to propagate.
- Around line 30-38: Update the file-loading logic in the produceState block so
the file.exists() check is performed inside withContext(Dispatchers.IO),
alongside BitmapFactory.decodeFile(). Remove the preceding takeIf existence
check while preserving the null handling and bitmap conversion behavior.

In `@app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt`:
- Around line 71-98: The discover-plugins IconButton and install
FloatingActionButton in ManagerScreen need idetooltips long-press support. Add
the established tooltip anchor and long-press handler to both controls, using
the appropriate tooltip identifiers and preserving the existing
UrlManager.openUrl and PluginManagerUiEvent.OpenFilePicker actions.
- Around line 63-70: Replace android.R.string.cancel in ManagerScreen’s
navigationIcon contentDescription with the resources module’s cd_navigate_back
string, and add that cd_navigate_back resource with the “Navigate back” text to
its strings.xml.

In
`@app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt`:
- Around line 118-143: Update the plugin action menu in PluginListItem so only
the enable/disable options remain guarded by plugin.isLoaded; render the
uninstall DropdownMenuItem for every listed plugin, preserving its existing
menuExpanded reset and onUninstall callback.

In
`@app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt`:
- Line 7: Replace the android.util.Log import and Log.w usages in
PluginManagerContent with an SLF4J LoggerFactory logger, using structured
placeholders and an appropriate warning level. Update all referenced locations,
including the additional occurrences, while preserving the existing messages and
values.
- Around line 163-174: In the OpenFilePicker branch handling
filePickerLauncher.launch, replace the broad Exception catch with an explicit
ActivityNotFoundException catch, add the required import, and log the caught
throwable before showing the existing no-file-manager error.
- Around line 57-58: Move the content-URI filename validation out of the picker
callback and into the relevant ViewModel using a background dispatcher, ensuring
Uri.getFileName is not called on the UI thread. Update the existing effect flow
to return the validation result and have the picker handling consume that
result, while preserving the current PLUGIN_EXTENSION matching behavior.

In
`@app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt`:
- Around line 126-130: Update the DetailRow composable to use a positional
detail-row format string defined in the resources module, and retrieve it with
stringResource while passing label and value as arguments. Remove the inline
"$label: $value" construction so translators can control ordering, spacing, and
punctuation.

In
`@app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt`:
- Around line 82-87: The template count currently uses a fixed plural string.
Update TemplateListItem.kt lines 82-87 to import and use pluralStringResource
with R.plurals.template_contains_count and item.templates.size; replace
resources/src/main/res/values/strings.xml line 1270’s template_contains_count
string with singular and plural forms in a plurals resource.
- Around line 61-64: Update the combinedClickable usage in TemplateListItem so
single-template cards are not treated as clickable or expose tap press
semantics. Apply click handling only when item.hasMultipleTemplates and
onViewTemplates are valid, while preserving onLongPressTooltip for long-press
behavior.

In
`@app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt`:
- Around line 33-34: Change the _uiEffect channel in TemplateManagerViewModel to
use buffering so effects emitted before a collector is ready are retained, and
update the existing viewModelScope emission paths to send through the channel
without dropping results. Add a test that emits an effect before collection
begins, then starts collecting and verifies the effect is received.

In
`@app/src/test/java/com/itsaky/androidide/templates/manager/models/CgtFileItemTest.kt`:
- Around line 3-6: Enable JUnit Jupiter for app unit tests and migrate
CgtFileItemTest to org.junit.jupiter.api.Test with Truth assertions, updating
its test annotations and assertion imports/usages. In
app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
at lines 3-7, retain JUnit 4 and RobolectricTestRunner compatibility while
replacing only its assertion imports/usages with Truth; do not migrate its Test
annotation to Jupiter.

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt`:
- Around line 89-109: The error flashbar presentation is duplicated across both
tab bodies. In
app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt:89-109
and
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt:133-153,
extract the shared logic into a ComponentActivity helper that accepts the
message resource, format arguments, and clip-label resource; replace both blocks
with calls to it, using the template and plugin clip labels respectively. Define
the 5000L duration as a named constant and preserve the conditional copy action
and indefinite duration behavior.

In `@app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt`:
- Around line 6-76: Add KDoc for the public contracts in
app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt:6-76,
covering TemplateManagerUiState, TemplateManagerUiEvent,
TemplateManagerUiEffect, and TemplateOperation, including state ownership,
effect delivery, destructive actions, and caller expectations. Document event
dispatch behavior and threading expectations for the relevant API in
app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt:40-49.
Document plugin action and tooltip callback contracts in
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt:45-55,
and add KDoc for each dialog’s confirmation and dismissal contract in
app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt:25-123.

In
`@app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt`:
- Line 3: Replace android.util.Log and the TAG-based logging in
TemplateManagerViewModel with the project’s SLF4J LoggerFactory facade. Update
all affected logging calls, including the referenced ranges, to use structured
placeholder arguments instead of interpolated messages, and remove the obsolete
TAG declaration/import.
🪄 Autofix

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 Plus

Run ID: 68cd254b-8078-46a4-bc86-93930cc11c63

📥 Commits

Reviewing files that changed from the base of the PR and between ba381bb and 755445a.

📒 Files selected for processing (32)
  • ARCHITECTURE.md
  • app/build.gradle.kts
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt
  • app/src/main/java/com/itsaky/androidide/app/DeviceProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
  • app/src/main/java/com/itsaky/androidide/di/PluginModule.kt
  • app/src/main/java/com/itsaky/androidide/di/TemplateModule.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepository.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/theme/ManagerTheme.kt
  • app/src/main/java/com/itsaky/androidide/ui/models/TemplateManagerUiState.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt
  • app/src/main/res/layout/activity_plugin_manager.xml
  • app/src/main/res/layout/dialog_install_plugin.xml
  • app/src/main/res/layout/item_plugin.xml
  • app/src/main/res/menu/menu_plugin_manager.xml
  • app/src/test/java/com/itsaky/androidide/templates/manager/models/CgtFileItemTest.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
  • gradle/libs.versions.toml
  • idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt
  • resources/src/main/res/values/strings.xml
💤 Files with no reviewable changes (4)
  • app/src/main/res/menu/menu_plugin_manager.xml
  • app/src/main/res/layout/item_plugin.xml
  • app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt
  • app/src/main/res/layout/dialog_install_plugin.xml

Comment thread app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
Comment thread app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt Outdated
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator Author

Code review

Found 2 issues:

  1. The Compose FAB never reads PluginManagerUiState.isInstalling, so it stays enabled during an install. The deleted PluginManagerActivity.updateUI() did binding.fabInstallPlugin.isEnabled = !state.isInstalling; nothing in ManagerScreen/PluginManagerContent replaces it, and there is no modal blocking the tap. isInstalling is still set around the install flow in PluginManagerViewModel (lines 245 and 307) but is now unread, so a second tap starts a concurrent installPlugin() coroutine.

},
floatingActionButton = {
if (pagerState.currentPage == TAB_PLUGINS) {
FloatingActionButton(
onClick = { pluginViewModel.onEvent(PluginManagerUiEvent.OpenFilePicker) },
) {
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = stringResource(R.string.cd_add),
)
}
}
},

  1. The cachedFilesDir warm-up adds an unguarded credential-encrypted storage read to DeviceProtectedApplicationLoader.load(), which runs on every start including Direct Boot. IDEApplication.cachedFilesDir is by lazy { instance.filesDir } (IDEApplication.kt#L155), and every other storage-touching call in this same function is wrapped in runCatching because "this may fail when running in direct boot mode". CredentialProtectedApplicationLoader.isCredentialStorageReady gates the same access on userManager.isUserUnlocked for this reason. app.coroutineScope is a bare MainScope() with no CoroutineExceptionHandler, so a throw here reaches handleUncaughtException and exitProcess(EXIT_CODE_CRASH) - the failure mode d98e51d55 (ADFA-2026) and dbb8cc05b (ADFA-2358, "IllegalArgumentException: Invalid path: /data/data/com.itsaky.androidide/files") were written to eliminate. Wrapping the block in runCatching matches the surrounding convention.

app.coroutineScope.launch(Dispatchers.IO) {
// early-init theme manager since it may need to perform disk reads
IThemeManager.getInstance()
// warm IDEApplication.cachedFilesDir off-main so later readers (e.g. pluginModule,
// resolved on the main thread on first navigation to the Extensions Manager) don't
// trip StrictMode's DiskReadViolation
IDEApplication.cachedFilesDir
}

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Comment thread app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt Outdated
@hal-eisen-adfa

Copy link
Copy Markdown
Collaborator Author

Doc drift: PLUGIN_AUTHORING.md still points at the deleted PluginListAdapter.kt

This PR deletes app/src/main/java/com/itsaky/androidide/adapters/PluginListAdapter.kt. docs/PLUGIN_AUTHORING.md points at that file three times. The PR does not change the doc, so each pointer now goes to a file that does not exist.

Line Current text Status after this PR
114 "renders a different icon based on whether the system is in light or dark mode (PluginListAdapter.kt:61)" Behavior is correct. The pointer is dead. The code moved to PluginListItem.kt:69-70.
142 "Icons are decoded with Glide (PluginListAdapter.kt:69), which handles raster formats only." Pointer is dead, and the decoder name is now wrong. FileImage.kt:36 calls BitmapFactory.decodeFile.
254 "The selection happens in PluginListAdapter.kt:61 via isSystemInDarkMode()." Behavior is correct. The pointer is dead.

This is not a pre-existing issue. The Glide sentence was true before this PR: the old adapter imported Glide and called Glide.with(pluginIcon).load(iconFile) (line 71 on stage). This PR replaces that call with BitmapFactory.decodeFile, so this PR is what makes the doc wrong. Glide itself stays in the module - TemplateListAdapter.kt still uses it, so the dependency is not orphaned.

Impact is moderate. A plugin author reads this doc to learn where to put icons and which formats to use. Both answers stay correct: BitmapFactory decodes PNG, WebP, and JPEG, and it does not decode SVG or vector XML, so the "raster formats only" rule survives the swap. Only the citations rot. The reader loses the ability to jump to the source; the reader does not build a broken plugin.

CLAUDE.md asks for the doc update in the same change:

Keep docs in step with code. When you change code, update the docs that describe it in the same change [...] so a doc never outlives the API it documents. If the doc fix is out of scope, file a ticket rather than let it drift.

This PR already follows that rule for ARCHITECTURE.md. PLUGIN_AUTHORING.md was missed.

Two ways to close it:

  1. Edit three lines here. PluginListAdapter.kt:61 becomes PluginListItem.kt:69; PluginListAdapter.kt:69 becomes FileImage.kt:36; "Glide" becomes "BitmapFactory".
  2. File an ADFA ticket for the doc update and link it in the PR description.

Option 1 costs less. The edit touches Markdown only, so it does not pull any Kotlin file under the Spotless ratchet.

🤖 Generated with Claude Code

@jatezzz

jatezzz commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Review: ADFA-4928 — single manager for plugins and templates

Read the full diff and verified against the surrounding code on stage.

Overview

Replaces the View-based Plugin Manager with a Compose two-tab "Manager" screen (Plugins | Templates) and adds a Templates feature end-to-end:

  • Compose enablement in :appkotlin.compose plugin, buildFeatures.compose, BOM + runtime/ui/foundation/material3/activity. First production Compose screen in the app (ADR 0009).
  • Plugins tab — faithful port: SAF install, enable/disable/uninstall, overwrite/details/restart dialogs. PluginListAdapter, item_plugin.xml, dialog_install_plugin.xml, menu_plugin_manager.xml deleted; activity_plugin_manager.xml reduced to a ComposeView + feedback FAB.
  • Templates tab — new CgtTemplateReader, CgtFileItem models, TemplateRepository(+Impl), TemplateManagerViewModel, templateModule. Scans Environment.TEMPLATES_DIR + Downloads; install/uninstall/delete.
  • Side fixIDEApplication.cachedFilesDir to dodge a StrictMode DiskReadViolation on pluginModule resolution.
  • ARCHITECTURE.md updated in the same change.

Solid work overall: UDF layering respected, the parser is deliberately Android-free and unit-tested, the KDoc explains the non-obvious calls, and the docs were updated alongside. Verified all referenced strings/drawables exist, the catalog already carried the Compose aliases, Robolectric reaches :app via projects.testing.unit, and no dangling references to the deleted files remain.


High — worth fixing before merge

1. Double system-bar insets.
EdgeToEdgeIDEActivity.onApplyWindowInsets documents "These insets are not expected to be consumed", and PluginManagerActivity.onApplySystemBarInsets pads the root FrameLayout by the full system-bar insets. View padding doesn't consume insets, so Compose still sees them: Scaffold's default contentWindowInsets (safeDrawing) and TopAppBar's default windowInsets (status bars) apply the same insets a second time. Expect a status-bar-height gap above the app bar and a nav-bar-height gap below the content. Either pass contentWindowInsets = WindowInsets(0) / windowInsets = WindowInsets(0) in ManagerScreen.kt, or drop onApplySystemBarInsets and let Compose own insets. Worth a device check either way given the "protect the two system bars" constraint.

2. Effect collection is no longer lifecycle-scoped.
The old activity used repeatOnLifecycle(STARTED). Both tabs now use a bare LaunchedEffect(viewModel) { viewModel.uiEffect.collect { … } } (PluginManagerContent.kt, TemplateManagerScreen.kt), which collects for as long as the composable is in composition — including while the activity is stopped. DialogUtils.showRestartPrompt(activity) and the flashbar builders then run against a stopped activity (BadTokenException territory), reachable if the user backgrounds the app while a plugin install finishes. Wrap with flowWithLifecycle / repeatOnLifecycle.

3. installTemplate silently overwrites and ignores a failed delete.

val dest = File(templatesDir, item.file.name)
item.file.copyTo(dest, overwrite = true)
item.file.delete()
  • No conflict prompt. A core.cgt sitting in Downloads silently replaces the bundled template — which the code elsewhere goes out of its way to protect (uninstallTemplate blocks BUNDLED). The plugin flow has ShowOverwriteConfirmation for exactly this; templates have nothing.
  • delete()'s return is discarded. If the Downloads copy survives, the next scan lists the same .cgt twice — once installed, once not. deleteDownloadFile checks the return; this path should too.

4. Rendezvous Channel + pager disposal drops effects.
TemplateManagerViewModel uses Channel<TemplateManagerUiEffect>() — default RENDEZVOUS, so trySend fails silently with no suspended receiver. HorizontalPager disposes the off-screen page along with its LaunchedEffect collector, which makes this concrete: init { loadTemplates() } runs when the VM is first resolved in setContent, long before the Templates tab is composed, so a scan failure emits ShowError into a channel nobody is receiving from and the user sees an unexplained empty list. Same for any effect emitted while the other tab is selected. Use Channel(Channel.BUFFERED). (PluginManagerViewModel has the same rendezvous channel on stage — pre-existing, but the tabbed layout is what makes it reachable.)


Medium

  • Dialog state lost on rotation. dialogState / selectedTemplateDetails use remember, not rememberSaveable. Rotating with the uninstall confirmation open silently dismisses it; tab switching drops it too, since the pager disposes the page.
  • Wrong TalkBack label on the back button. ManagerScreen.kt uses contentDescription = stringResource(android.R.string.cancel) → TalkBack announces "Cancel" for a navigate-up affordance.
  • Long-press tooltip invisible to accessibility services. Modifier.pointerInput { detectTapGestures(onLongPress = …) } produces no semantics node, so TalkBack users can't reach the tooltip at all; View.setOnLongClickListener at least surfaced via the local context menu. Suggest semantics { onLongClick(...) } or combinedClickable(onLongClickLabel = …).
  • TooltipTag.TEMPLATE_MANAGER has no content. The constant is added, but tooltip bodies live in the external documentation DB and nothing seeds "template.manager" — long-pressing the Templates tab shows an empty/failed tooltip. Needs a DB entry or a follow-up ticket.
  • isLoading is never rendered. TemplateManagerUiState defaults to isLoading = false with an empty list, so the screen flashes "No templates found" before the first scan lands, and there's no indicator during install/uninstall. Default it to true.
  • SAF filter narrowed from */* to application/octet-stream. The KDoc acknowledges it's an approximation, but a .cgp is a zip — providers reporting application/zip (or cloud providers with their own mapping) will now hide valid plugin files with no way to pick them. The old */* had no false negatives. Consider arrayOf("application/octet-stream", "application/zip", "*/*").

Low / polish

  • Duplicated version formatterpluginVersionLabel (PluginListItem.kt) and versionLabel (CgtFileItem.kt) are the same logic with subtly different blank handling. Collapse to one.
  • TemplateOperation is dead code — never referenced, and uses inline java.io.File FQNs instead of an import.
  • CgtTemplateReader.parseOptionalTags can be private — tests only exercise readTemplates.
  • template_contains_count ("Contains %1$d templates") should be a <plurals>.
  • Three names for one screen — preference title "Extensions Manager", top bar "Plugins & Templates", and title_plugin_manager ("Plugin Manager") now unused; delete it. plugin_manager_title's English text changed but the values-zh-rCN / values-in-rID translations are now semantically stale.
  • uninstallTemplate restores with overwrite = true into Downloads, silently clobbering a same-named file there.
  • FileImage decodes without inSampleSize — an oversized plugin icon can OOM. Glide (used by the deleted adapter) handled downsampling; a bounds pass would restore that.
  • cachedFilesDir warm-up is unguarded. It resolves instance.filesDir (credential-protected) from DeviceProtectedApplicationLoader; if that phase can run pre-unlock the access throws and crashes the coroutine. The sibling IThemeManager.getInstance() is equally unguarded so it matches existing style, but a runCatching around both is cheap insurance. Worth confirming the loader always runs post-unlock.
  • Compose BOM 2024.02.00 (Compose 1.6.1 / Material3 1.2.0, ~2 years old) paired with the Kotlin 2.3.0 Compose compiler plugin. It'll work, but as the first consumer this PR is the natural place to bump it. The catalog's compose-compiler = "2.1.21" pin is now unused — remove or wire it.

Test coverage

Good: CgtTemplateReaderTest is genuinely thorough — multi-template archives, missing template.json, optional tags with and without identifiers, and the lenient/unquoted-key JSON the shipped core.cgt actually uses. The Robolectric annotation with a comment explaining why (org.json stubs) is exactly right. CgtFileItemTest covers the pure helpers well.

Gaps:

Security

Nothing alarming. takePersistableUriPermission handling is preserved; CgtTemplateReader only reads zip entries (no extraction, so no zip-slip). One note: zip.readBytes() on a template.json entry is unbounded, so a malicious .cgt with a huge entry could OOM the app — low severity for a deliberately imported file, but a size cap is cheap.

Conventions

Tabs/LF and ktlint formatting look correct throughout; strings correctly land in resources/src/main/res/values/strings.xml; the Koin module follows the existing pluginModule shape; no new dependencies beyond what the catalog already declared. The PluginModule.kt reformat is bundled with behavioral changes — minor, but per the "mechanical commits separate from behavioral" guidance it'd read better split.

yaturner and others added 5 commits August 5, 2026 10:58
Address CodeRabbit review feedback on PR #1627:
- Use SLF4J logging instead of android.util.Log
- Narrow runCatching to expected I/O/parsing exceptions, rethrowing
  CancellationException instead of swallowing it
- Refuse to install/uninstall over an existing same-name destination
  file instead of silently overwriting it
- Treat a failed source-file delete as an install/uninstall failure
  and roll back the copied destination file
Address CodeRabbit review feedback on PR #1627:
- Warm IDEApplication.cachedFilesDir on an IO thread before Koin starts,
  eliminating the race where pluginModule/templateModule could resolve it
  on the main thread first
- Bound FileImage's bitmap decode with inSampleSize and move the
  file-existence check inside the IO dispatcher; narrow its catch to
  recoverable failures and let CancellationException propagate
- Move the picked plugin file's name/extension validation (a
  ContentResolver IPC call for content:// URIs) off the picker
  callback and into PluginManagerViewModel on a background dispatcher,
  routed back through a new ShowInstallConfirmation effect
- Replace android.util.Log with SLF4J logging in PluginManagerContent
- Narrow the file-picker launch catch to ActivityNotFoundException and
  log it instead of silently swallowing any Exception
Address CodeRabbit review feedback on PR #1627:
- Avoid double system-bar insets by zeroing ManagerScreen's Scaffold
  contentWindowInsets, since the activity's root already applies them
- Fix the back button's TalkBack announcement (was "Cancel") with a
  dedicated cd_navigate_back string
- Wire long-press tooltips to the discover-plugins action and install FAB
- Always show Uninstall for a listed plugin, even when it failed to
  load, so a broken plugin has a recovery action
- Move the detail-row "label: value" format into a string resource so
  translators control ordering/punctuation
- Only treat a template card as clickable when it bundles more than
  one template, instead of always exposing tap/press semantics
- Use an Android plurals resource for the template count string
  instead of a fixed "templates" string
- Buffer TemplateManagerViewModel's uiEffect channel and use send()
  instead of trySend() so effects aren't dropped before a collector
  is ready
Address CodeRabbit review feedback on PR #1627: document the model
contracts, including the meaning of installed/provenance and the
one-archive-to-many-templates relationship.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address CodeRabbit review feedback on PR #1627 (matches the JUnit
Jupiter + Truth strategy ARCHITECTURE.md already documents for unit
tests, which the app module hadn't wired up yet):
- Run app unit tests on the JUnit Platform, with the vintage engine
  so existing JUnit 4/Robolectric tests keep running unchanged
- Migrate CgtFileItemTest (no Robolectric dependency) to
  org.junit.jupiter.api.Test with Truth assertions
- Keep CgtTemplateReaderTest on JUnit 4/RobolectricTestRunner (no
  built-in Jupiter integration) but switch its assertions to Truth

Verified all 22 app unit test classes still run under
:app:testV8DebugUnitTest with 0 failures.

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

Caution

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

⚠️ Outside diff range comments (1)
app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt (1)

51-53: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard template reloads against stale results.

loadTemplates() launches a new coroutine for every request from init, onEvent, and post-mutation success paths. Since templateRepository.listTemplateFiles() runs on Dispatchers.IO without synchronization or a request token, a faster initial load can complete after a later mutation-triggered reload and replace uiState.items with stale data. Serialize reloads or ignore results from obsolete jobs, and cover out-of-order load completion in a coroutine test.

🤖 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/viewmodels/TemplateManagerViewModel.kt`
around lines 51 - 53, Update loadTemplates and its callers so overlapping reload
requests cannot apply stale listTemplateFiles results: serialize loads or track
and discard obsolete jobs, while preserving the loading state and post-mutation
refresh behavior. Add a coroutine test that completes concurrent loads out of
order and verifies uiState.items retains the newest result.
🧹 Nitpick comments (2)
app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt (2)

40-48: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the public event API.

onEvent is public but has no KDoc. Document its event contract, lifecycle-bound execution, state updates, and one-shot effects.

As per coding guidelines, public functions must document contracts, threading, nullability, side effects, or units.

🤖 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/viewmodels/TemplateManagerViewModel.kt`
around lines 40 - 48, Document the public TemplateManagerViewModel.onEvent
function with KDoc covering its accepted TemplateManagerUiEvent contract,
lifecycle-bound execution, resulting state updates, and one-shot effects; do not
change the event handling behavior.

Source: Coding guidelines


82-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the project logger instead of Log.

Replace the structured logging calls in app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt with existing SLF4J LoggerFactory logger calls and keep exceptions as throwable arguments. Also applies to lines 102 and 129.

🤖 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/viewmodels/TemplateManagerViewModel.kt`
at line 82, Replace the Android Log calls in TemplateManagerViewModel, including
the failures near lines 82, 102, and 129, with the existing project SLF4J
LoggerFactory logger. Preserve each message and pass the caught exception as the
throwable argument to the logger call, removing the direct Log dependency if no
longer used.

Source: Coding guidelines

🤖 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/app/IDEApplication.kt`:
- Around line 199-203: Move the cachedFilesDir warmup out of the pre-branch
startup path and execute it only after the user-unlocked initialization via
DeviceProtectedApplicationLoader.load(). Ensure onCreate() does not evaluate
cachedFilesDir during Direct Boot, while preserving the existing IO-thread
warmup once credential-protected storage is available.
- Around line 199-203: Remove the blocking runBlocking call around
cachedFilesDir from Application.onCreate(). Replace it with non-blocking
initialization, or defer/guard the Koin pluginModule/templateModule resolution
so cachedFilesDir is accessed only after the Activity framework can continue
startup.

In `@app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt`:
- Around line 47-52: In the icon-loading catch blocks of FileImage, add
rate-limited SLF4J warning logs for handled SecurityException and
OutOfMemoryError cases before returning null. Use the established observability
mechanism, include the failure context and exception, and do not log the file
path; preserve CancellationException propagation and placeholder fallback
behavior.
- Around line 85-93: Update the sampling loop in decodeBounded() to base
inSampleSize on the larger of bounds.outWidth and bounds.outHeight, allowing
sampling whenever that maximum dimension remains at least twice maxDimensionPx.
Preserve the existing power-of-two increments and decode options flow.

In `@app/src/main/java/com/itsaky/androidide/ui/models/PluginManagerUiState.kt`:
- Around line 55-57: Update the PluginManagerUiEffect channel to use
Channel.BUFFERED so one-time effects survive periods when the LaunchedEffect
collector is unavailable, and handle failed trySend results by logging or
reporting the delivery failure. Preserve the existing ShowInstallConfirmation
effect flow and locate the changes around the channel declaration and its send
sites.

In `@common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt`:
- Around line 11-27: Update Uri.getFileName to catch only verified recoverable
ContentResolver provider failures in the second catch, while retaining the
existing SecurityException handling; do not convert unrelated exceptions into
"Unknown File". Replace the current UriExtensions logging with a class-scoped
SLF4J logger and use it for handled failures, preserving the fallback label only
for genuinely recoverable query failures.

---

Outside diff comments:
In
`@app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt`:
- Around line 51-53: Update loadTemplates and its callers so overlapping reload
requests cannot apply stale listTemplateFiles results: serialize loads or track
and discard obsolete jobs, while preserving the loading state and post-mutation
refresh behavior. Add a coroutine test that completes concurrent loads out of
order and verifies uiState.items retains the newest result.

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt`:
- Around line 40-48: Document the public TemplateManagerViewModel.onEvent
function with KDoc covering its accepted TemplateManagerUiEvent contract,
lifecycle-bound execution, resulting state updates, and one-shot effects; do not
change the event handling behavior.
- Line 82: Replace the Android Log calls in TemplateManagerViewModel, including
the failures near lines 82, 102, and 129, with the existing project SLF4J
LoggerFactory logger. Preserve each message and pass the caught exception as the
throwable argument to the logger call, removing the direct Log dependency if no
longer used.
🪄 Autofix

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 Plus

Run ID: 5bdff30c-2ed4-4788-9852-1b49aa06cafe

📥 Commits

Reviewing files that changed from the base of the PR and between 755445a and 46231d6.

📒 Files selected for processing (18)
  • app/build.gradle.kts
  • app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/models/PluginManagerUiState.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/TemplateManagerViewModel.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/models/CgtFileItemTest.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
  • common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
  • gradle/libs.versions.toml
  • resources/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (11)
  • app/build.gradle.kts
  • app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt
  • gradle/libs.versions.toml
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerDialogs.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/models/CgtFileItemTest.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt

Comment thread app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt Outdated
Comment thread common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
Address CodeRabbit follow-up review feedback on PR #1627:
- Only run the cachedFilesDir warmup eagerly in onCreate() when
  credential-protected storage is already unlocked - the default
  Context.getFilesDir() throws during Direct Boot. When locked, warm it
  instead from CredentialProtectedApplicationLoader.load(), which only
  proceeds once that storage is confirmed accessible.
- Base FileImage's inSampleSize loop on the larger image dimension
  instead of requiring both dimensions to exceed the target, so a
  wide-but-short (or tall-but-narrow) image still gets downsampled
- Log FileImage's swallowed SecurityException/OutOfMemoryError icon-load
  failures via a throttled SLF4J warning, without logging the file path
- Buffer PluginManagerViewModel's uiEffect channel and use send() instead
  of trySend(), same fix already applied to TemplateManagerViewModel, so
  effects (e.g. the new ShowInstallConfirmation) aren't dropped
- Narrow UriExtensions.getFileName's second catch to SecurityException/
  IllegalArgumentException instead of blanket Exception, so unexpected
  ContentResolver failures surface instead of being silently mislabeled
  as "Unknown File" (and then downstream as an unsupported plugin file);
  switch its logging to a class-scoped SLF4J logger

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

🤖 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/ui/compose/common/FileImage.kt`:
- Around line 106-107: Update decodeBounded() to reject maxDimensionPx values
less than or equal to zero before entering the inSampleSize loop, using the
existing error-handling contract for invalid inputs. Preserve the current
sampling behavior for positive limits and ensure FileImage() cannot trigger the
loop with a zero limit.
🪄 Autofix

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 Plus

Run ID: e7b8a4ea-29e5-4ce1-a3b2-8d71d1e8ff53

📥 Commits

Reviewing files that changed from the base of the PR and between 46231d6 and 0c9406c.

📒 Files selected for processing (5)
  • app/src/main/java/com/itsaky/androidide/app/CredentialProtectedApplicationLoader.kt
  • app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt
  • common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
  • app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
  • app/src/main/java/com/itsaky/androidide/viewmodels/PluginManagerViewModel.kt

Comment thread app/src/main/java/com/itsaky/androidide/ui/compose/common/FileImage.kt Outdated
- Disable the install FAB while a plugin install is in flight, so a
  second tap can't start a concurrent installPlugin() coroutine. The
  Compose ManagerScreen replaced the old Activity, which disabled the
  FAB via binding.fabInstallPlugin.isEnabled = !state.isInstalling;
  nothing carried that behavior over.
- Fix PLUGIN_AUTHORING.md pointers left dangling by the
  PluginListAdapter.kt -> PluginListItem.kt/FileImage.kt migration.

The delete-failure-handling and cachedFilesDir warmup comments from
the same review were already addressed by prior commits on this
branch; verified against current HEAD, no further changes needed.

@hal-eisen-adfa hal-eisen-adfa left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Blocking review: 5 findings from a fresh pass

These are new - none overlap the CodeRabbit threads or the two earlier review comments. Details inline; #2 has two sites (TemplateRepositoryImpl, UriExtensions).

  1. The long-press tooltips added to the FAB and the toolbar action almost certainly never fire.
  2. Two "narrow the catch" fixes turned swallowed failures into crash paths, because both callers are bare viewModelScope.launch with no CoroutineExceptionHandler.
  3. The label_value string-resource fix landed in the plugin dialog but not the template one.
  4. PluginManagerActivity's try/catch no longer covers anything, since setContent's lambda runs after onCreate returns.
  5. pluginVersionLabel duplicates the tested versionLabel and already disagrees with it on blank input.

(GitHub does not allow REQUEST_CHANGES on your own PR, so this is submitted as a comment review; treat each inline as blocking.)

Comment thread app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt Outdated
Comment thread common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt Outdated
Comment thread app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt Outdated
…ct collection

- ManagerScreen's TopAppBar still used its default status-bar insets on
  top of PluginManagerActivity.onApplySystemBarInsets, which already
  pads the root view by the full system-bar insets (that padding
  doesn't consume the insets, so Compose saw them a second time). Zero
  out TopAppBar's windowInsets to match the Scaffold's
  contentWindowInsets, which was already zeroed.
- PluginManagerContent and TemplateManagerScreen collected
  viewModel.uiEffect in a bare LaunchedEffect, so it kept collecting
  while the activity was stopped. A plugin install finishing while the
  app is backgrounded could then run DialogUtils.showRestartPrompt or a
  flashbar builder against a stopped activity. Wrap both collectors in
  repeatOnLifecycle(STARTED), matching the old repeatOnLifecycle(STARTED)
  pattern used elsewhere in the app.
- Long-press tooltips on the FAB and Discover-plugins IconButton never
  fired: pointerInput(detectTapGestures) placed on the caller-side
  modifier loses the down event to the button's own internal clickable,
  which runs first on the Main pointer pass. Drive the tooltip off the
  button's own MutableInteractionSource instead (press duration vs
  LocalViewConfiguration's longPressTimeoutMillis), which observes the
  same press stream the button already dispatches rather than racing it
  for the raw pointer event.
- CgtTemplateReader.readTemplates read a zip entry's bytes unbounded,
  so a corrupt/hostile .cgt sitting in the public Downloads folder could
  OOM the app; bound the read and throw IOException past 1 MiB.
  parseCgtFile also didn't catch IllegalArgumentException, which
  ZipInputStream.nextEntry throws for a non-UTF-8 entry name - that
  propagated out of the bare viewModelScope.launch in
  TemplateManagerViewModel.loadTemplates (no CoroutineExceptionHandler)
  and crashed the app. Both are now handled per-file, so one bad archive
  is skipped instead of failing the whole scan.
- UriExtensions.getFileName's catch was narrowed to
  SecurityException/IllegalArgumentException, but a misbehaving content
  provider can throw other RuntimeExceptions from query()/getString()
  (CursorWindowAllocationException, a wrapped DeadObjectException, ...).
  Broadened back to Exception, since this is a best-effort display-name
  lookup, not a path that should ever crash the caller.
- TemplateManagerDialogs' DetailRow still built "$label: $value" with
  string concatenation instead of the R.string.label_value fix that
  landed in the plugin dialog, and the optional-tags list hardcoded a
  non-ASCII "*" bullet in code (CLAUDE.md's ASCII rule). Added
  R.string.template_optional_tag and reused R.string.label_value.
  TemplateListItem's status/provenance row had the same
  hardcoded-separator shape; extracted it to R.string.label_separator.
- PluginManagerActivity's try/catch around setContent no longer caught
  anything: setContent only registers the composable, and its lambda
  (where both ViewModels first resolve via Koin) runs at first layout,
  after onCreate has already returned past the catch. Force-resolve
  both `by viewModel()` delegates inside the try, before setContent.
- PluginListItem.pluginVersionLabel duplicated CgtFileItem.versionLabel
  and disagreed with it on blank input (a stray "v" chip vs the tested
  ""). Deleted the duplicate and reused the tested helper.

Added a CgtTemplateReaderTest regression case covering the bounded-read
cap. Verified via :app:testV8DebugUnitTest (all passing) and
spotlessCheck.
@yaturner

Copy link
Copy Markdown
Contributor

@hal-eisen-adfa Addressed all 6 findings from your review at 9d124ac:

  1. Long-press tooltips on the FAB / Discover-plugins IconButton - moved off the competing pointerInput/detectTapGestures detector to the button's own MutableInteractionSource (press duration vs. longPressTimeoutMillis), so it no longer races the button's internal clickable.
  2. CgtTemplateReader/TemplateRepositoryImpl crash paths - bounded the unbounded readBytes() (throws past 1 MiB) and added the missing IllegalArgumentException catch for malformed zip entry names; both now skip the one bad file instead of failing the whole scan.
  3. UriExtensions.getFileName - broadened the catch back to Exception, since it's a best-effort lookup with no caller-side guard.
  4. Template dialog string parity - DetailRow now uses R.string.label_value; the optional-tags bullet and the status/provenance separator are no longer hardcoded in code.
  5. PluginManagerActivity's try/catch - both ViewModels now resolve inside the try, before setContent, so a Koin/Environment failure is caught again instead of crashing.
  6. pluginVersionLabel - deleted; PluginListItem now calls the tested versionLabel and handles a blank version without the stray "v" chip.

Ran :app:testV8DebugUnitTest (added a regression test for the bounded-read cap) and spotlessCheck, both clean. Could you take another look when you get a chance?

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- follow-up commit ---'
git show --stat --oneline 9d124ac96
git diff 9d124ac96^ 9d124ac96 -- \
  app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt \
  app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt \
  app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt \
  common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt \
  app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt \
  app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt \
  resources/src/main/res/values/strings.xml

echo '--- structural maps ---'
for f in \
  app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt \
  app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt \
  app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt \
  common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt \
  app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt \
  app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt
do
  echo "--- $f ---"
  ast-grep outline "$f" --items all || true
done

echo '--- relevant source ranges ---'
for f in \
  app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt \
  app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt \
  app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt \
  common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt \
  app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt \
  app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt \
  app/src/main/java/com/itsaky/androidide/templates/manager/models/CgtFileItem.kt
do
  echo "--- $f ---"
  cat -n "$f"
done

echo '--- references and resource declarations ---'
rg -n -C 3 --glob '*.kt' \
  'TooltipTag\.(PLUGIN_MANAGER|TEMPLATE_MANAGER)|longPressTimeoutMillis|MutableInteractionSource|versionLabel\(|pluginVersionLabel|CgtTemplateReader\.readTemplates|parseOptionalTags|getFileName\(' \
  app common idetooltips || true
rg -n -C 2 'label_value|template_optional|template_status|template_provenance' \
  resources/src/main/res/values/strings.xml || true

Length of output: 96508


@yaturner I found one remaining issue in the tooltip change.

  • Long press still triggers the button action. rememberLongPressInteractionSource() observes the press state, but it does not consume the gesture or prevent the button click. After the timeout, it shows the tooltip. When the user releases the button, IconButton still opens Discover Plugins and FloatingActionButton still opens the file picker. A long press must show only the tooltip. Use a gesture handler that suppresses onClick after a completed long press, or track long-press completion and ignore the next button click.

The bounded archive read, malformed ZIP handling, filename lookup fallback, string-resource changes, ViewModel resolution boundary, and shared version formatter look correct from static inspection.

You are interacting with an AI system.

@jimturner-adfa jimturner-adfa self-assigned this Aug 10, 2026

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

🧹 Nitpick comments (1)
app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt (1)

39-45: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Restrict the onCreate recoverable failure boundary.

The broad catch (e: Exception) around super.onCreate(), setContent, ViewModel initialization, and setupFeedbackButton() treats unrelated lifecycle or post-configuration failures as recoverable plugin-manager init errors. Catch only documented initialization failures and log handled errors with LoggerFactory instead of printStackTrace(); report handled failures through the project observability path as applicable.

🤖 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/PluginManagerActivity.kt`
around lines 39 - 45, Narrow the try/catch in PluginManagerActivity.onCreate to
cover only documented plugin-manager initialization failures, rather than
super.onCreate(), setContent, and setupFeedbackButton() or unrelated lifecycle
errors. Replace printStackTrace() with LoggerFactory logging and route handled
failures through the project’s observability mechanism where applicable, while
preserving recovery for the expected ViewModel/configuration initialization
failure.

Sources: Coding guidelines, Learnings

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

Nitpick comments:
In `@app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt`:
- Around line 39-45: Narrow the try/catch in PluginManagerActivity.onCreate to
cover only documented plugin-manager initialization failures, rather than
super.onCreate(), setContent, and setupFeedbackButton() or unrelated lifecycle
errors. Replace printStackTrace() with LoggerFactory logging and route handled
failures through the project’s observability mechanism where applicable, while
preserving recovery for the expected ViewModel/configuration initialization
failure.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a5d2a847-c635-4ac9-b992-da7da7a6680c

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7be22 and 9d124ac.

📒 Files selected for processing (12)
  • app/src/main/java/com/itsaky/androidide/activities/PluginManagerActivity.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
  • common/src/main/java/com/itsaky/androidide/utils/UriExtensions.kt
  • resources/src/main/res/values/strings.xml
🚧 Files skipped from review as they are similar to previous changes (10)
  • app/src/main/java/com/itsaky/androidide/ui/compose/ManagerScreen.kt
  • app/src/test/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReaderTest.kt
  • resources/src/main/res/values/strings.xml
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginManagerContent.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerDialogs.kt
  • app/src/main/java/com/itsaky/androidide/repositories/TemplateRepositoryImpl.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateListItem.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/templates/TemplateManagerScreen.kt
  • app/src/main/java/com/itsaky/androidide/ui/compose/plugins/PluginListItem.kt
  • app/src/main/java/com/itsaky/androidide/templates/manager/parsing/CgtTemplateReader.kt

… bound

CodeRabbit flagged (2026-08-05 review, still unresolved) that
decodeBounded()'s inSampleSize loop assumes maxDimensionPx > 0. If it's
ever <= 0 - e.g. the 40.dp default rounding to a sub-pixel size at an
unusual density - the loop condition (a non-negative quotient >= a
non-positive bound) is permanently true, hanging on an unbounded
doubling of inSampleSize instead of throwing. Skip the downsampling
loop entirely in that case and decode at inSampleSize = 1.

@hal-eisen-adfa hal-eisen-adfa left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Third pass: 2 residuals

The six findings from the last review all check out - the IllegalArgumentException catch, the 1 MiB bounded read, the broadened UriExtensions catch, the label_value/separator string resources, the pre-setContent ViewModel resolution, and the pluginVersionLabel deletion are all correct. My two earlier issue comments (the unread isInstalling, the PLUGIN_AUTHORING.md drift) are closed too.

Two things are still open, both inline:

  1. The cachedFilesDir warm-up is gated but still not wrapped in runCatching, so a non-lock-state filesDir failure still exits the process.
  2. The long-press tooltip now fires, but it doesn't suppress the button's click - a long press on the FAB shows the tooltip and opens the file picker.

(Submitted as a comment review because GitHub does not allow REQUEST_CHANGES on your own PR; treat #2 as blocking.)

// instead runs from CredentialProtectedApplicationLoader.load(), which only proceeds once
// that storage is confirmed accessible.
if (isUserUnlocked) {
runBlocking(Dispatchers.IO) { cachedFilesDir }

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Still needs runCatching.

Gating on isUserUnlocked closes the Direct Boot path from my Aug-4 comment, and that was the larger half of it. What the gate does not cover is a filesDir failure that is not lock-state related - which is the failure this codebase has actually shipped a crash for. ADFA-2358 (dbb8cc05b) was IllegalArgumentException: Invalid path: /data/data/com.itsaky.androidide/files; the user is unlocked, the gate passes, and the call still throws.

Both warm-up sites are unprotected:

  • Here. runBlocking(Dispatchers.IO) { cachedFilesDir } rethrows into onCreate, before ensureKoinStarted(). Nothing upstack catches it, so it lands in handleUncaughtException -> exitProcess(EXIT_CODE_CRASH) (DeviceProtectedApplicationLoader.kt:166).
  • CredentialProtectedApplicationLoader.kt:84. withContext(Dispatchers.IO) { IDEApplication.cachedFilesDir } runs inside coroutineScope.launch(Dispatchers.Default) (IDEApplication.kt:94), a bare MainScope() with no CoroutineExceptionHandler - same destination. It also sits before the _isLoaded.compareAndSet on line 86, so a throw there leaves the flag false and the whole remainder of credential-protected init silently never runs.

What makes this worth fixing rather than arguing about: cachedFilesDir is a by lazy warm-up whose entire purpose is to keep a later main-thread read off the disk. Failing it should degrade to "the first real read pays the syscall" - never to a process exit. That is exactly why every other storage-touching call in DeviceProtectedApplicationLoader.load() is wrapped in runCatching with the comment "this may fail when running in direct boot mode, so we wrap this in runCatching and ignore errors" (lines 57, 61, 76). This block is the odd one out.

if (isUserUnlocked) {
	runCatching { runBlocking(Dispatchers.IO) { cachedFilesDir } }
		.onFailure { logger.warn("Failed to warm cachedFilesDir; first read will hit disk", it) }
}

Swallowing it is safe in both directions: by lazy caches the value, not the failure, so a throw here does not poison later reads and the retry from CredentialProtectedApplicationLoader still works.

}
},
modifier = Modifier.alpha(if (pluginUiState.isInstalling) DISABLED_ALPHA else 1f),
interactionSource = rememberLongPressInteractionSource { showTooltip() },

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Blocking. The tooltip fires now, but so does the action.

The interactionSource switch is the right fix for detection - it observes the same press stream the button already dispatches instead of competing for the raw pointer event, so the race from my last review is gone. It does not suppress the click, though, and Modifier.clickable has no long-press concept at all. Traced through the versions this PR builds against (material3 1.2.0, foundation 1.6.1):

  • IconButton.kt:85-94 - modifier.<...>.clickable(onClick = onClick, interactionSource = interactionSource, ...)
  • FloatingActionButton.kt:104-112 -> Surface(onClick, interactionSource) -> Surface.kt:235-240 - .clickable(interactionSource, indication, enabled, onClick)
  • Clickable.kt:981 routes plain clickable to detectTapAndPress, which is awaitFirstDown() then waitForUpOrCancellation() then onTap?.invoke(...) (TapGestureDetector.kt:237-255). No duration threshold anywhere in that path.

So a long press on the FAB shows the tooltip at longPressTimeoutMillis and then fires OpenFilePicker when the finger lifts; on the Discover action it shows the tooltip and then launches the browser. The pre-Compose activity did neither - setOnLongClickListener returning true consumed the event.

combinedClickable is what suppresses it: with a non-null onLongClick it routes to detectTapGestures (Clickable.kt:1019), where the long-press branch invokes onLongPress and then consumeUntilUp() with upOrCancel left null, so onTap is never reached (TapGestureDetector.kt:128-138). That is why PluginListItem.kt:58 and TemplateListItem.kt:66 are correct as written.

It cannot simply be moved onto the caller modifier here, though - FloatingActionButton/IconButton keep their own internal clickable at the tail of the chain, so you would have two competing detectors and would reintroduce the bug I filed last time. Two options that do work:

  1. Drop the Material wrapper where you need both gestures. Replace IconButton with a Box(Modifier.size(48.dp).clip(CircleShape).combinedClickable(onClick = ..., onLongClick = ...)) around the Icon, so exactly one detector owns the node. Cleanest semantically, and it keeps a single source of truth for the gesture; costs you the built-in ripple/sizing defaults, which you would restore via indication/size.
  2. Keep rememberLongPressInteractionSource and consume the next click. Have the helper set a flag when it fires and have onClick read-and-clear it:
val suppressNextClick = remember { mutableStateOf(false) }

// in rememberLongPressInteractionSource's LaunchedEffect, after delay(longPressTimeoutMillis):
suppressNextClick.value = true
currentOnLongPress()

// at the call site:
onClick = {
	if (suppressNextClick.value) {
		suppressNextClick.value = false
	} else {
		pluginViewModel.onEvent(PluginManagerUiEvent.OpenFilePicker)
	}
}

The ordering is safe - the flag is set at the long-press timeout, strictly before the up event that drives onTap.

Option 1 for the IconButton and option 2 for the FAB is probably the least-churn split, since there is no drop-in replacement for a Material FAB's shape/elevation.

Either way this still wants the device check I asked for last time: the unit-test run does not exercise any of this. Worth confirming on hardware that a long press shows the tooltip and leaves the file picker closed, and that a normal tap still opens it.

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.

4 participants