ADFA-4128 (11/11): app + bench — wiring Quick Build into the IDE and the benchmark harness - #1723
ADFA-4128 (11/11): app + bench — wiring Quick Build into the IDE and the benchmark harness#1723fryanpan wants to merge 13 commits into
Conversation
5b48f90 to
c69d8ef
Compare
c69d8ef to
5a3d5eb
Compare
There was a problem hiding this comment.
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.
5a3d5eb to
ac3ab4e
Compare
ac3ab4e to
a45a359
Compare
|
@coderabbitai review |
|
a45a359 to
7c72105
Compare
3e8d9d2 to
6254159
Compare
jatezzz
left a comment
There was a problem hiding this comment.
@fryanpan — review of the Quick Build app wiring. Seven findings; two are worth fixing before merge and are left as inline comments:
ProjectHandlerActivity.kt:549— thereArmInstallsafety net does not cover the clobber dialog, so a rotation while it is up loses the install silently.QuickBuildStatusBar.kt:151— a landed build is re-announced on every re-subscribe and, withonlyIfOwned = false, stomps project-init / plugin-install status.
The remaining five are low: the session teardown on a transient APK parse failure, the null-activity path that builds stale content, the ellipsized actionable status copy, and three unused imports that should fail spotlessCheck.
One more (low), which could not be left inline because the file is not in this diff:
app/src/main/java/com/itsaky/androidide/actions/BaseBuildAction.kt:43 — a missed sibling of this PR's raw-vs-user-visible split.
This PR moved every UI decider over to isUserVisibleBuildInProgress — AbstractCancellableRunAction, ProjectHandlerActivity.onResume, the progress bar, BuildVariantsFragment — but BaseBuildAction.prepare still reads the raw flag:
enabled = buildService?.let { !it.isBuildInProgress } == trueWith the experiments flag on, Quick Build's eager prebuild now runs on every project open, so RunTasksAction (and any other direct BaseBuildAction) is silently greyed out for its whole duration with no explanation — unlike QuickBuildAction, which relabels, or QuickRunAction, which flashes msg_build_slot_busy. Note that AbstractCancellableRunAction.prepare unconditionally re-sets enabled = true, which is why its new slot-busy flash is reachable and these are not.
Checked and cleared: all new string/drawable/menu resources resolve on the head branch; ProjectManagerImpl.generateSources() returns Boolean, so GenerateSourcesDeferral's refusal-retry contract holds and a throw is treated as a refusal rather than cancelling the scope; InternalBuildBracket releases strictly after isBuildInProgress clears, so there is no window where isUserVisibleBuildInProgress reads true for an internal build; QuickBuildOutputNarrator confines its mutable state to one Dispatchers.Main.immediate scope and compares sink identity correctly; QuickBuildReloadTimingMetric.asBundle() is 25 params worst case, within the Firebase cap; ThermalSafeStrategy copies GradleDaemonConfig, so the new non-defaulted daemonIdleTimeoutMs is safe and 2h fits in Int; and InstallationResultHandler.onResult returning null is handled as "do nothing" by its only caller.
| if (isDestroyed || isFinishing) { | ||
| return@launch | ||
| } | ||
| dispatched = true |
There was a problem hiding this comment.
@fryanpan medium — the re-arm safety net does not cover the clobber dialog.
dispatched = true is set before the dialog is shown, so finally { if (!dispatched) buildViewModel.reArmInstall(state) } treats "dialog opened" as "install dispatched".
Scenario: a Standard Run build finishes, the "Replace the app installed for this project?" dialog appears, the user rotates the device. The dialog is dismissed along with the activity, onConfirm never fires, doInstallApk never runs, and nothing re-arms AwaitingInstall. A successful build ends with no install and no message — the exact failure mode the re-arm was added to prevent.
Suggest keeping dispatched = false until a decision is actually reached, so the net also covers "dialog shown but dismissed without a decision".
| ): QuickBuildStatusBarUpdate? = | ||
| when { | ||
| // A duration means a build landed - the moment BUILD FAILED must be overwritten. | ||
| current.buildDurationMillis != null -> { |
There was a problem hiding this comment.
@fryanpan medium — a landed build is re-announced on every re-subscribe, and it stomps other status.
This current.buildDurationMillis != null branch is tested before the previous == null branch and emits Show(..., onlyIfOwned = false).
The status collector runs in repeatOnLifecycle(STARTED) with a local previousStatus reset to null on every re-subscribe, and QuickBuildStatus is a StateFlow, so it replays. Every time the user returns to the editor after a Quick Build has landed, the bar re-writes "Quick Build: reloaded to generation N in 2.3s" for a build that finished long ago — and because onlyIfOwned = false, it clobbers whatever took the bar in the meantime (project-init / plugin-install status).
That directly contradicts the KDoc above, which says "a 'Project initialized' message is not stomped by a session that has nothing to say."
Fix either way: require previous != null for the landed-build branch, or pass onlyIfOwned = true when previous == null.
| // The Quick Build session's installed baseline is about to be replaced; stop it. | ||
| // Keyed off the re-check rather than off whether a dialog was shown: a tap that | ||
| // already confirmed this exact clobber skips the dialog but still clobbers. | ||
| if (now != QuickBuildClobberConfirmation.NotNeeded) { |
There was a problem hiding this comment.
@fryanpan low — a transient APK parse failure tears down a healthy session.
onProceed calls restartSession() whenever now != NotNeeded, but NeededForUnknownAppId is produced by any runCatching failure in apkApplicationId — including a transient packageManager.getPackageArchiveInfo failure on the freshly built APK.
Only is Needed actually asserts that the proxy app occupies the slot. As written, a parse hiccup kills a live, healthy Quick Build session for no established reason. Consider restarting only on is Needed.
|
|
||
| val activity = data.getActivity() | ||
| if (activity == null) { | ||
| sessionManager.onQuickBuildTapped() |
There was a problem hiding this comment.
@fryanpan low — the null-activity path skips the save-all flush and builds stale content.
When data.getActivity() returns null this calls sessionManager.onQuickBuildTapped() directly, bypassing the save-all flush and passing the default wroteSomething.
That is exactly the "build silently uses stale on-disk content while the editor shows the user's edit" case the surrounding ~20 lines of comment exist to prevent, and it also feeds the session's armed-switch heuristic the wrong signal. Returning false (or flashing, as the other paths do) would be safer than building stale.
| android:id="@+id/statusText" | ||
| android:layout_width="0dp" | ||
| android:layout_height="wrap_content" | ||
| android:ellipsize="end" |
There was a problem hiding this comment.
@fryanpan low — actionable copy gets truncated; fails the 2x font-scale bar.
statusText is maxLines="1", and this diff adds ellipsize="end" to it — then multi-sentence actionable copy is routed through it, e.g. quick_build_status_app_not_running ("Quick Build: built. Your app is not running - tap Quick Build to start it with your changes.") and quick_build_status_needs_full_build.
At 2x font scale on a phone this truncates well before the instruction, so the one surface QuickBuildStatusBar's KDoc says exists to name the remedy cannot show the remedy. CLAUDE.md reserves maxLines/ellipsize for text that is genuinely disposable; these strings are not.
| import org.greenrobot.eventbus.Subscribe | ||
| import org.greenrobot.eventbus.ThreadMode.MAIN |
There was a problem hiding this comment.
@fryanpan low — unused imports; spotlessCheck should fail on this.
Neither Subscribe nor ThreadMode.MAIN is used — there is no @Subscribe member anywhere in this file. ktlint's standard:no-unused-imports is on by default and this file is in the Spotless ratchet, so CI should reject it. Worth re-running spotlessApply against the final state of the branch (see also the unused Position import in EditorHandlerActivity.kt).
| import com.itsaky.androidide.models.FileExtension | ||
| import com.itsaky.androidide.models.OpenedFile | ||
| import com.itsaky.androidide.models.OpenedFilesCache | ||
| import com.itsaky.androidide.models.Position |
There was a problem hiding this comment.
@fryanpan low — unused import.
Position appears only on this import line; the identifier is never used in the file. Same standard:no-unused-imports / Spotless-ratchet issue as the two EventBus imports added to ProjectHandlerActivity.kt.
6254159 to
7e90fff
Compare
7e90fff to
1f82366
Compare
…adds the debug-only benchmark harness The app wiring and the bench surface land together because they are mutually dependent: :app's ProjectHandlerActivity and QuickBuildModule call into QuickBuildBenchHooks, and QuickBuildBenchHooks returns AutostartBuild and resolves EnvironmentQuickBuildPaths. Neither ordering of a two-PR split compiles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W
…ent, test gaps Important 1 (daemon idle-timeout/Metaspace tuner un-gated): kept un-gated by design — the 384m Metaspace floor fixes real OOM-killed builds and the tiered idle timeouts keep low-RAM devices from losing the IDE to lmkd; GradleBuildTuner now states this in its KDoc. Ships flag-off; needs Bryan sign-off in the PR body. Important 2 (generateSources narrowing un-gated): judged a genuine all-users improvement, not QB-specific — the old code ran a Gradle generateSources after EVERY save-all (and after any XML save in SaveFileAction), a per-save build tax; flag-off the deferral degenerates to the same immediate call, so the narrowing is the only behavior change. Known trade (manifest-only edits leave generated Manifest/R intermediates stale until the next resource save or build) now stated at both call sites. Ships flag-off; needs sign-off in the PR body. Important 3 (install dropped on rotation, flag on): installApk's async path now re-arms AwaitingInstall (BuildViewModel.reArmInstall, fires only from Idle) from the coroutine's drop path, so a configuration change during the APK-manifest parse makes the recreated activity's collector retry the install instead of silently losing a successful build. Covered by BuildViewModelInstallReArmTest. Test gap (zip-slip guard): extraction loop extracted to QuickBuildArtifactStager.extractDaemonZip(InputStream, File); the guard is watched going red by QuickBuildArtifactStagerTest (a ../ entry throws and nothing lands outside the daemon dir). Test gap (InstallationEventFlow mapping): InstallationEventFlowTest pins the PackageInstaller status mapping, including the ABORTED-vs-FAILURE branch order and the no-extras / no-status paths. Test gap (service-side output capture): suppress/capture/drain routing extracted from GradleBuildService.logOutput into InternalBuildOutputCapture; bounded tail, drain-clears, throwing progress listener and editor-listener routing pinned by InternalBuildOutputCaptureTest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W
…tiple The Gradle daemon idle-timeout comment quoted a speedup multiple, which put a benchmark figure into shipping production code. The reason the timeout is generous is structural - a warm daemon skips the cold start, which dominates a short rebuild - so the comment now says that instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W
- F1723-1 put QuickBuildPipelineTest into the suite that actually runs - F1723-4 guard the deferred prebuild fire() against a throw Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
The script has had one commit and predates several behaviour changes, so a walker following it literally hits steps that cannot reach their stated end state. None of these are product defects - the walk found no product failure. - T7 criteria 4/5 described the pre-b6cddf035 world where rebaseline left the app un-relaunched and a tap was needed. Rebaseline relaunches now. - T7b step 2 and T11 step 3 end at a modal OS install prompt that never times out; "do not tap anything" could not reach the end state. - T1 gains a FAB baseline tap. Five later tests assert through the FAB, so a dead FAB failed them all with no way to tell when it broke. - T1 gains a note that project creation already ran a setup build, so the first tap measures warm provisioning, not cold. - T14's "no reinstall unless the bytes changed" inverted the design: the generation stamp lives in the APK, so a restart always mints new bytes. - T20 names service-app; only 4 of 30 corpus apps declare a Service. - T21 drops "wrap and push sora-editor-full first" - already wrapped, with all 288 source files. Adds a "Traps that make the product look broken" section for the four method errors that produced wrong findings: tapping the geometric centre of a view that extends under a system bar, Find-in-file being a regex search, relaunching CoGo via monkey when it declares two LAUNCHER activities, and selecting a wrapped corpus copy by mtime when the newest is pinned to AGP 9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
Six places where a Quick Build doc contradicted the code it describes. Each was re-verified against the source rather than taken from the review comment. - debugging.md: the deploy round-trip row said the 15 s bound covers "one AIDL onPayload call". IQuickBuildTarget is a oneway interface, so that call returns immediately; DeployChannel wraps the call plus the wait for a generation-matched report, which is what its own KDoc already said. - why-not-android-jar.md: listed native libs as hot-loadable. A .so under jniLibs forces a Gradle fallback (ChangeClassifier). Loadable at runtime and changeable via live reload are different properties. - reliability-gaps.md: "five user-facing defects" against three fixed and four open. Seven were surfaced; the fixed three are relink-stuck, #88 and #90. Also states why Blocks v1? reads TBD - the decision at the top is a proposal, and the cells become "No" when it is confirmed. - low-spec-devices.md: stated an inferred mechanism (SerialGC thrashing in a small heap) as the confirmed cause of the 1.9 GB failure. The outcome is measured; the mechanism is not, and the uncapped run that would confirm it is still unmeasured. Retitled to what was actually observed. - concurrency.md: the tap-races-its-own-save section read as current behaviour. It describes the pre-2026-08-13 design that the redesign below it replaced. - perf-roadmap.md: incomplete sentence. Not applied: CodeRabbit's finding that manual-qa.md's screenrecord --time-limit 1740 is invalid because AOSP caps at 180 s. False on our hardware - recordings of 1774 s, 2432 s, 2592 s, 2842 s and 3534 s have all completed on the A56, and the surrounding comment already documents the real 30-minute cap that 1740 sits under. Applying it would break working recordings. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
None of these is a Quick Build failure - the walk passed every test. They are
places where the product is correct and unhelpful.
A2 - the bolt read identically to a screen reader in READY and ERROR. Every tone
has its own icon shape, so a sighted user can tell them apart; collapsing them
all to "Quick Build" hid that distinction from exactly the user who cannot see
the icon. ERROR, SLOW and RECONNECTING now announce their state. BUILDING and
the standard-build-blocked case already did.
A3 - after an undeliverable build the bar read "built, but could not be
delivered - see Build Output" on every poll, while the sentence naming the fix
("Your app is not running. Tap Quick Build to start it with your changes.") was
only in Build Output. The bar now names the tap when that is the whole problem.
Carried as a typed flag rather than matched on the message text, the same way
proxyAppNotConnected already is, and kept separate from it because they mean
opposite things: appNotRunning is "nobody opened it", proxyAppNotConnected is
"we launched it and it still did not arrive".
A4 - an orphaned proxy app reported CoGo's expected connect() rejection at W on
every attempt of the rebind backoff loop, 14 times in one restart window. The
behaviour is right (it continues standalone); repeating an expected rejection at
W buries the entries around it. Reported once per streak now, cleared by a
successful connect.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
The page proposed that #87, #89, #91 and the relink-crash gap go to v1.1, then left the table's "Blocks v1?" column reading TBD on all four rows. A proposal in a title and a TBD in a table say different things to a reader, and CodeRabbit flagged the pair as an internal inconsistency. Decision confirmed 2026-08-25: none of the four block v1. The four cells now read "No - v1.1", the title states the answer rather than asking it, and the prose no longer describes itself as awaiting confirmation. No change to any gap's evidence, root cause, or fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FstXxJ5cwWPcvmhZ9vJgJ7
…ing branch F1713-3 (docs/concurrency.md): the thesis said every expensive thing runs in another process while the table directly below it put the mtime poll and the install call on Dispatchers.IO inside CoGo. Name the exception. F1713-8 (docs/manual-qa.md): files are not killed, processes are - and a teammate follows this runbook literally while holding a half-recorded QA session. Say screenrecord. Both patch text that exists only in the four trailing commits, so they could not ship until those commits had a home. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC
…ded Cancel Both found by running the CodeRabbit CLI over this branch ourselves, since the pull request's 119 files exceed the bot's 100-file cap. - installApk consumed the tap-time clobber answer before entering the coroutine, so the answer left the ViewModel whether or not the install went on to dispatch. A rotation during the manifest parse cancels the coroutine, the finally block re-arms the install, and the retry then ran with no tap-time answer - asking the user to confirm the same overwrite a second time. The method's own KDoc says the re-check is silent unless the answer moved, which is precisely what this broke. The consume now sits inside the coroutine after the destroyed check, on the path that actually dispatches; there is no suspension point between it and the dispatch. Verified with a throwaway harness rather than a committed test: it drove the real BuildViewModel and installTimeClobberConfirmation through both orderings with a real cancellation, showed the old ordering re-asking and the new one silent, and was watched going red under a mutated expectation. It is not committed, because ProjectHandlerActivity is abstract and untested by any of the 64 JVM test files in app/src/test, so a committed version would model the ordering rather than read it and would stay green if the line moved back. The ordering is guarded by review only. - QuickBuildScreen.declineClobberConfirm matched a hard-coded English "cancel" while its sibling acceptClobberConfirm resolved its label from resources, so the decline path alone broke on a non-English device. Both confirm paths reach one builder, which sets android.R.string.cancel, so the framework string is the right resource. Compiles; runtime behaviour on a non-English locale is unverified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC
…e phone banner Bryan pinned the wording on 2026-08-28 for the phone banner; the CoGo-side notice for the same event still said "Your app crashed... Fix the crash and save", which sends the user to fix code that was fine. The state is only ever set from failReload, so the reload machinery failed, never their code. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xsc7AMGBVyEMfrwpZX87iC
…ring qb-08's review fix added QuickBuildMessage.ProvisioningFailedUnexpectedly, the named case for a provisioning throw with no message of its own. The exhaustive when in QuickBuildMessages.resolve had no arm for it, so the restacked qb-11 would not compile. Maps it to quick_build_provisioning_failed_unexpectedly, worded like the neighbouring quick_build_setup_failed, and pins the mapping in QuickBuildMessagesTest alongside the other valueless cases. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STCsdMzx9daNBcqMN424Ci
Entry #89 (failed daemon respawn strands the session) was written against the prototype and went stale: the missing QuickBuildTapped arm in reduceDegraded landed with qb-08's review fixes, a failed respawn now dispatches DaemonRestartFailed and surfaces a message, and qb-07's trim-memory redesign no longer bumps the daemon epoch. Move #89 from the open list to fixed-on-this-branch, update the counts and decision line, and state what remains: no device repro on either side of the fix, so the recovery arms are host-tested only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VwVV7PzMYinSiq6FwC83Vw
1f82366 to
5f92da7
Compare
Part 11/11 of the stacked split of #1669 (requested by Akash). Base: feature/ADFA-4128-qb-10-gradle-plugin. Stack overview + review mechanics: PR 1 (#1713). Terms are defined in quickbuild/README.md (lands in PR 1).
Puts Quick Build in front of the user: a button next to Run, and enough narration to tell what it is doing and when it has finished. It also adds a harness to make it easier to run standard Gradle build and Quick Build benchmarks, and to gather key metrics about the stages of the build process.
flowchart TB subgraph appc["<b>This PR: inside app/ — wiring and bench</b>"] act["QuickBuildAction<br/>registered only when<br/>FeatureFlags.isExperimentsEnabled<br/><i>QuickBuildAction.kt</i>"] --> mgr["QuickBuildManager<br/>session lifecycle, provisioning,<br/>stop-tap cancellation"] mgr --> narr["QuickBuildOutputNarrator<br/>attached to the session manager;<br/>queues while no pane is bound<br/><i>QuickBuildOutputNarrator.kt</i>"] mgr --> sb["status bar collector<br/>lifecycle-scoped: state, not history<br/><i>QuickBuildStatusBar.kt</i>"] koin["QuickBuildModule (Koin)<br/>binds every core port;<br/>assetsLiveReloadable read once<br/>at the Android edge<br/><i>QuickBuildModule.kt</i>"] tr["bench trampoline activity<br/>debug-source-set manifest only<br/><i>QuickBuildBenchActivity.kt</i>"] --> mgr mgr --> hooks["QuickBuildBenchHooks<br/>inert release twin<br/><i>debug/QuickBuildBenchHooks.kt</i>"] hooks --> rec["event + metrics recorders"] rec --> log["bench-events.jsonl<br/><i>BenchEventsFile.kt</i>"] hooks --> e2e["MODE_STANDARD_E2E<br/>measures the standard build<br/>through install + launch<br/><i>QuickBuildBenchAutostart.kt</i>"] end adb["adb shell am start<br/>gated on android.permission.DUMP"] --> tr mgr --> core[":quickbuild:core session manager (PRs 5-8)"] narr --> pane["Build Output pane (existing)"] sb --> bar["bottom status bar (existing)"] mgr -- "provisioning + rebuild builds" --> gbs["GradleBuildService (existing)"] classDef thisPrBox fill:#dbeafe,stroke:#93c5fd,color:#1e3a5f classDef inPr fill:#ffffff,stroke:#64748b,color:#000 class appc thisPrBox class act,mgr,narr,sb,koin,tr,hooks,rec,log,e2e inPrWhat to review
QuickBuildAction.kt— owns the session tap: start, stop-tap cancel, grey-out. Line-by-line.Gradle tuning — Metaspace 192→384 MB + daemon idle timeouts (30 min balanced / 2 h high-perf); the only changes to non-QB behavior
QuickBuildOutputNarrator.kt,QuickBuildStatusBar.kt— queued narration; lifecycle-scoped status showing state, not history.QuickBuildModule.kt— binds every core port; reads assetsLiveReloadable at the Android edge.GenerateSourcesDeferral.kt— defers resource-XML generateSources until Quick Build goes idle.John's items C15, C16, C17, C22, C23 folded in as fixes.
Rollback: without the flag there is no UI entry point.
Followup, not fixed: R8 emits kotlin.Metadata warning noise.
QuickBuildBenchAutostart.kt— MODE_STANDARD_E2E measures the standard build through install and launch. Line-by-line.The e2e latch bypasses install confirmations only for the measured span.
QuickBuildBenchActivity.kt,QuickBuildBenchHooks.kt— DUMP-gated trampoline; inert release twin.BenchEventsFile.kt— a failed relaunch omits relaunchOk rather than recording zero.How this PR Was Tested
Automated tests (see coverage details below)
Manual QA — walked the
manual-qa.mdtest plan on the A56 [measured on a56]Benchmark — measured on real devices, both arms: a warm code edit reaches the running app with about a 5x median speedup over a standard build + deploy. The weaker the phone, the bigger the win. MODE_STANDARD_E2E drove the standard arm through install and launch.
Still open — the rebaseline relaunch path is not yet device-verified, and neither is the API 28/29 resource-swap success path.
Coverage (JaCoCo at the stack tip, single run):
A lot of this was UI code and wasn't covered very well by automated tests.
actions/buildactions/fileactivities/editoranalytics/quickbuildappApplicationclasses, Android-bounddifragments/sidebarhandlersquickbuildservices/builderService, Android-boundutilsviewmodelReview fixes (2026-08-22)
A review-fixes commit addresses the code-review findings. Two changes here deliberately ship to all users, with the Experiments flag off (approved):
One candidate followup from review (orchestrator forcing a full-changed compile after a failed dex/deploy) was re-checked and refuted at this tip: the forced flag re-arms and a forced no-op already performs the full rebuild. The daemon-side recovery lever stays in as defense in depth.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Kj9YeCDHGp9DU8LPtfWJ7W