From 58c83968de06ef546fa4bfea1544baa962ab0110 Mon Sep 17 00:00:00 2001 From: Kurt Himebauch <136133082+xXJSONDeruloXx@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:19:23 -0500 Subject: [PATCH] feat(agent): a setup step that prepares fast Lab builds for the installed version, kept alive while it runs --- .../com/droiddeck/launcher/agent/AgentHost.kt | 2 +- .../launcher/agent/AgentHostService.kt | 20 ++++++++++++++++++- .../droiddeck/launcher/agent/AgentSetup.kt | 17 +++++++++++++--- .../droiddeck/launcher/ui/AgentSetupView.kt | 10 ++++++++-- app/src/main/res/values/strings.xml | 5 +++++ 5 files changed, 47 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentHost.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentHost.kt index 666d6f756..20a243840 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentHost.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentHost.kt @@ -378,7 +378,7 @@ object AgentHost { now = SystemClock.elapsedRealtime() appContext?.let { app -> lab = LabProgress.read(LinuxRuntime.rootDir(app)) - if (state == State.WORKING) AgentHostService.update(app) + AgentHostService.update(app) } main.postDelayed(::tick, if (state == State.WORKING || lab != null) TICK_MS else IDLE_TICK_MS) } diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentHostService.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentHostService.kt index 7a3072679..86419e69d 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentHostService.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentHostService.kt @@ -18,6 +18,7 @@ class AgentHostService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent?.action == ACTION_STOP) { + if (AgentHost.lab?.running == true) AgentHost.cancelLabBuild(this) AgentHost.stop(this) return START_NOT_STICKY } @@ -30,19 +31,35 @@ class AgentHostService : Service() { private const val NOTIFICATION_ID = 4107 private const val ACTION_STOP = "com.droiddeck.launcher.agent.STOP" private var lastText: String? = null + @Volatile private var started = false + /** Setup steps that keep the app alive while they run, besides the agent's turn. */ + private var setupSteps = 0 fun start(context: Context) { lastText = null + started = true runCatching { context.startForegroundService(Intent(context, AgentHostService::class.java)) } } fun stop(context: Context) { + if (setupSteps > 0) return + started = false context.stopService(Intent(context, AgentHostService::class.java)) } + fun setupStarted(context: Context) { + setupSteps++ + start(context) + } + + fun setupEnded(context: Context) { + setupSteps = (setupSteps - 1).coerceAtLeast(0) + if (AgentHost.state != AgentHost.State.WORKING) stop(context) + } + /** Refreshes the notification with the current step; unchanged text is not re-posted. */ fun update(context: Context) { - if (AgentHost.state != AgentHost.State.WORKING) return + if (!started) return val text = describe(context) if (text == lastText) return lastText = text @@ -52,6 +69,7 @@ class AgentHostService : Service() { private fun describe(context: Context): String { val lab = AgentHost.lab?.takeIf { it.running } if (lab != null) return context.getString(R.string.agent_lab_progress, lab.permille / 10, minutes(lab.etaSeconds.toLong())) + if (AgentHost.state != AgentHost.State.WORKING) return context.getString(R.string.agent_setting_up) val tool = AgentHost.running() val elapsed = minutes((SystemClock.elapsedRealtime() - AgentHost.turnStarted) / 1000) return if (tool != null) "${tool.tool} · ${tool.detail}".take(120) + " · $elapsed" else context.getString(R.string.agent_thinking_for, elapsed) diff --git a/app/src/main/java/com/droiddeck/launcher/agent/AgentSetup.kt b/app/src/main/java/com/droiddeck/launcher/agent/AgentSetup.kt index 5601c1cad..ea4934377 100644 --- a/app/src/main/java/com/droiddeck/launcher/agent/AgentSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/agent/AgentSetup.kt @@ -17,7 +17,7 @@ import java.util.concurrent.TimeUnit * and to make Lab builds on the device. */ object AgentSetup { - enum class Step(val required: Boolean) { CONNECTION(true), INSTALL(true), SIGN_IN(true), GITHUB(false), BUILD_TOOLS(false) } + enum class Step(val required: Boolean) { CONNECTION(true), INSTALL(true), SIGN_IN(true), GITHUB(false), BUILD_TOOLS(false), PREPARE(false) } enum class Status { UNKNOWN, DONE, TODO, WORKING, FAILED } val status = mutableStateMapOf() @@ -48,8 +48,8 @@ object AgentSetup { for (step in Step.entries) if (status[step] != Status.WORKING) { when (step) { Step.INSTALL -> set(step, if (AgentHost.guestFileExists(app, harness.binary)) Status.DONE else Status.TODO) - Step.BUILD_TOOLS -> set(step, if (AgentHost.guestFileExists(app, "/root/aapt2/aapt2") && - AgentHost.guestFileExists(app, "$CHECKOUT/.git")) Status.DONE else Status.TODO) + Step.BUILD_TOOLS -> set(step, if (buildToolsInstalled(app)) Status.DONE else Status.TODO) + Step.PREPARE -> set(step, if (prepared(app)) Status.DONE else Status.TODO) Step.CONNECTION -> probes += step to listOf("sh", "-c", "for i in 1 2 3; do adb connect \"\$ADB_SERIAL\" >/dev/null 2>&1; " + "adb -s \"\$ADB_SERIAL\" get-state 2>&1 | tee /dev/stderr | grep -q device && exit 0; sleep 1; done; exit 1") @@ -74,9 +74,11 @@ object AgentSetup { Step.GITHUB -> listOf("dd-gh-login") Step.BUILD_TOOLS -> listOf("sh", "-c", "set -e; [ -d $CHECKOUT/.git ] || git clone --filter=blob:none $REPO $CHECKOUT; " + "cd $CHECKOUT; [ -x tools/agent/setup ] || { echo 'This DroidDeck version has no tools/agent/setup yet'; exit 1; }; tools/agent/setup") + Step.PREPARE -> listOf("sh", "-c", "cd $CHECKOUT && dd-lab-build --prepare") } set(step, Status.WORKING) detail.remove(step) + AgentHostService.setupStarted(app) Thread({ val result = runCatching { if (step == Step.INSTALL) installHelpers(app) @@ -89,6 +91,7 @@ object AgentSetup { child.waitFor() } main.post { + AgentHostService.setupEnded(app) if (result.getOrNull() == 0) { detail.remove(step) status[step] = Status.UNKNOWN @@ -101,6 +104,14 @@ object AgentSetup { }, "agent-setup-${step.name.lowercase()}").start() } + fun buildToolsInstalled(app: Context) = AgentHost.guestFileExists(app, "/root/aapt2/aapt2") && AgentHost.guestFileExists(app, "$CHECKOUT/.git") + + /** dd-lab-build compiled this install's commit, so a Lab build compiles only what changed. */ + private fun prepared(app: Context) = runCatching { + File(LinuxRuntime.rootDir(app), "root/.cache/dd-lab-build-base").readText().trim() + .startsWith(com.droiddeck.launcher.lab.LabOverlay.apkCommit.take(7)) + }.getOrDefault(false) + /** Drops what the checks found, which belonged to the previous harness. */ fun forget() { status.clear() diff --git a/app/src/main/java/com/droiddeck/launcher/ui/AgentSetupView.kt b/app/src/main/java/com/droiddeck/launcher/ui/AgentSetupView.kt index 924ed59aa..270450542 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/AgentSetupView.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/AgentSetupView.kt @@ -65,7 +65,10 @@ internal fun AgentSetupView(modifier: Modifier, onDone: () -> Unit = {}) { steps.forEachIndexed { index, step -> val status = AgentSetup.status[step] ?: Status.UNKNOWN val (title, hint) = labels(step, AgentHost.harness.name) - val detail = AgentSetup.detail[step] ?: hint + val lab = AgentHost.lab?.takeIf { it.prepare && it.running } + val detail = if (step == Step.PREPARE && lab != null) + stringResource(R.string.agent_prepare_progress, lab.permille / 10, clock(lab.etaSeconds.toLong().coerceAtLeast(0))) + else AgentSetup.detail[step] ?: hint CheckRow( when (status) { Status.DONE -> CheckState.OK @@ -74,7 +77,8 @@ internal fun AgentSetupView(modifier: Modifier, onDone: () -> Unit = {}) { }, title, detail, divider = index < steps.lastIndex, ) { - val blocked = step == Step.SIGN_IN && AgentSetup.status[Step.INSTALL] != Status.DONE + val blocked = (step == Step.SIGN_IN && AgentSetup.status[Step.INSTALL] != Status.DONE) || + (step == Step.PREPARE && AgentSetup.status[Step.BUILD_TOOLS] != Status.DONE) if ((status == Status.TODO || status == Status.FAILED) && !blocked) { val label = stringResource(if (status == Status.FAILED) R.string.agent_setup_retry else action(step)) val run = { if (step == Step.CONNECTION) pair(context) else AgentSetup.run(context, step) } @@ -151,6 +155,7 @@ private fun labels(step: Step, harness: String): Pair = when (st Step.SIGN_IN -> stringResource(R.string.agent_step_sign_in, harness) to stringResource(R.string.agent_step_sign_in_hint) Step.GITHUB -> stringResource(R.string.agent_step_github) to stringResource(R.string.agent_step_github_hint) Step.BUILD_TOOLS -> stringResource(R.string.agent_step_build) to stringResource(R.string.agent_step_build_hint) + Step.PREPARE -> stringResource(R.string.agent_step_prepare) to stringResource(R.string.agent_step_prepare_hint) } private fun action(step: Step): Int = when (step) { @@ -158,6 +163,7 @@ private fun action(step: Step): Int = when (step) { Step.INSTALL -> R.string.agent_action_install Step.SIGN_IN, Step.GITHUB -> R.string.agent_action_sign_in Step.BUILD_TOOLS -> R.string.agent_action_install + Step.PREPARE -> R.string.agent_action_prepare } /** The pairing DroidDeck already uses for the phantom process fix; the agent reuses its key. */ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 40602b137..8654bdd79 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1164,6 +1164,7 @@ Failed: %1$s %1$s ended (exit %2$d) Stopped + Setting up the agent Rebuild my Lab build %1$s for the installed version of DroidDeck and restart on it. %1$s in · %2$s out Thinking @@ -1220,6 +1221,10 @@ Optional, to open pull requests Build tools Optional, for Lab builds on this device · about 5 GB + Fast Lab builds + Compiles this version ahead, so Lab builds take minutes · about 20 minutes once + Compiling · %1$d%% · about %2$s left + Prepare Pair Install Sign in