Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .papercuts/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,10 @@ owns; reopen the terminal before judging the final live state.
platform-independent test that exercises both certificate and keychain paths.
- A changelog search conflated the stable and prerelease lines. Verify published
package code before assuming a release contains the upstream patch.

## 2026-09-06 — cloud Node engines

- `package.json` engines require Node `>=22.19`. Some cloud images still expose
`/exec-daemon/node` at 22.14.0. Put `~/.nvm/versions/node/v22.22.2/bin` first
on `PATH` before `npm install` or `npm test`.

6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ npm install
npm run dev
```

The native Aiden On The Go iPhone and iPad client lives in [`ios/`](ios/README.md). It is not part of the Electron development command; open `ios/AidenOnTheGo.xcodeproj` or use the documented physical-device `xcodebuild` commands separately.
The native Aiden On The Go iPhone and iPad client lives in [`ios/`](ios/README.md). The Android validation client lives in [`android/`](android/README.md). Neither is part of the Electron development command; open `ios/AidenOnTheGo.xcodeproj` or the Android Gradle project separately.

### Mobile distribution

The iPhone and iPad app is distributed through **TestFlight only**. GitHub releases do not publish an IPA; [`ios/README.md`](ios/README.md) documents local development and device validation. Android validation remains separate from the macOS release. Pull requests run the Android verification gates without retaining an installable artifact; relevant merges to `main` publish the debug APK and its checksum.
The iPhone and iPad app is distributed through **TestFlight only**. GitHub releases do not publish an IPA; [`ios/README.md`](ios/README.md) documents local development and device validation. Android validation remains separate from the macOS release. Pull requests run the Android verification gates without retaining an installable artifact; relevant merges to `main` publish the debug APK and its checksum. Public Android availability is planned for **October 2026**.

The development launcher prepares a cached, ad-hoc-signed **Aiden Agent Dev** runtime that can run beside the installed **Aiden Agent** app. Development uses separate Application Support, Chromium session, log, crash, and `~/.aiden-dev` roots; it does not copy production data, register global shortcuts, or check the production update feed by default. Set `AIDEN_DEV_GLOBAL_SHORTCUTS=1` only when a development run intentionally needs the global bindings.

Expand Down Expand Up @@ -122,6 +122,8 @@ The checked-in models.dev snapshot is refreshed only through `npm run models:ref

Aiden Agent is a beta macOS release. Signed DMG and ZIP builds, checksums, and automatic-update metadata are published through [GitHub Releases](https://github.com/sambitcreate/aiden-agent/releases). The release workflow is fail-closed: it verifies signing, notarization, package contents, updater metadata, and version monotonicity before publishing. See [the release guide](docs/releasing.md) for the complete process.

macOS 1.0 and the Android companion are planned for **October 2026**. Until then macOS stays on the signed GitHub beta, Android stays a `main`-published debug APK for validation, and iPhone/iPad stays Internal TestFlight-only.

The canonical website download is the stable
[`Aiden-Agent-Beta-arm64.dmg`](https://github.com/sambitcreate/aiden-agent/releases/latest/download/Aiden-Agent-Beta-arm64.dmg)
alias from the latest public release. The historical
Expand Down
27 changes: 27 additions & 0 deletions android/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Aiden On The Go (Android)

Android is a debug-APK validation client for Aiden Agent on macOS. The Mac owns execution, persistence, providers, workspaces, and permissions. This app is a paired remote-control surface over a local network or Tailscale.

Public Android availability is planned for **October 2026**. Until then, pull requests run the Android verification gates without retaining an installable artifact; relevant merges to `main` publish the debug APK and its checksum. Do not treat this tree as a Play Store release.

## Current contract

- `applicationId` `sbtbiswas.AidenOnTheGo`, `versionName` `0.1.0`, `versionCode` `1`
- `AidenAppVersion.NAME` is `BuildConfig.VERSION_NAME` (Gradle `versionName` is the source of truth)
- Release minify stays off
- Pairing `deviceType` remains `iphone` or `ipad` until the OpenAPI contract, Mac, and iOS change together
- Manual setup codes use Crockford Base32 without `I` or `L`

Open the Gradle project in Android Studio, or from this directory run:

```sh
./gradlew :app:testDebugUnitTest :app:lintDebug
```

Pairing needs a reachable Mac HTTPS endpoint such as a Tailscale Serve URL. Do not use `127.0.0.1` on the phone; that address is the Android device itself.

The product and protocol sources of truth are:

- [`../docs/plans/aiden-on-the-go-plan.md`](../docs/plans/aiden-on-the-go-plan.md)
- [`../docs/aiden-remote-api-v1.md`](../docs/aiden-remote-api-v1.md)
- [`../protocol/aiden-remote/v1/openapi.json`](../protocol/aiden-remote/v1/openapi.json)
2 changes: 1 addition & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ android {
buildFeatures {
compose = true
aidl = false
buildConfig = false
buildConfig = true
shaders = false
}

Expand Down
2 changes: 2 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Add project-specific ProGuard rules here.
# Release minify remains off until a signed Play package is prepared.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package sbtbiswas.AidenOnTheGo

object AidenAppVersion {
val NAME: String = BuildConfig.VERSION_NAME
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,21 @@ fun AidenPairingScreen(
val activeId by installationStore.activeInstallationId.collectAsState()

var manualCode by remember { mutableStateOf("") }
var endpointUrl by remember { mutableStateOf("https://127.0.0.1:8765/api/aiden/v1") }
var endpointUrl by remember { mutableStateOf("") }
var qrJsonInput by remember { mutableStateOf("") }
var selectedTab by remember { mutableStateOf(0) } // 0: Scan QR, 1: Setup Code, 2: Paste JSON
var isPairing by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
var installationPendingRemoval by remember { mutableStateOf<AidenInstallation?>(null) }

fun formatCrockfordCode(input: String): String {
val clean = input.uppercase().replace("-", "").filter { it in "0123456789ABCDEFGHJKMNPQRSTVWXYZIL" }.take(20)
val clean = input.uppercase().replace("-", "").filter { it in "0123456789ABCDEFGHJKMNPQRSTVWXYZ" }.take(20)
val chunks = clean.chunked(4)
return chunks.joinToString("-")
}

fun handleScannedQRCode(scannedText: String) {
if (isPairing) return
scope.launch {
isPairing = true
errorMessage = null
Expand Down Expand Up @@ -284,12 +285,13 @@ fun AidenPairingScreen(

when (selectedTab) {
0 -> {
// Live Camera QR Code Scanner
AidenQRCodeScanner(
onCodeScanned = { scanned ->
handleScannedQRCode(scanned)
}
)
if (!isPairing) {
AidenQRCodeScanner(
onCodeScanned = { scanned ->
handleScannedQRCode(scanned)
}
)
}
if (isPairing) {
Spacer(modifier = Modifier.height(12.dp))
Row(
Expand Down Expand Up @@ -326,6 +328,7 @@ fun AidenPairingScreen(
value = endpointUrl,
onValueChange = { endpointUrl = it },
label = { Text("Mac Address (HTTPS Endpoint)") },
placeholder = { Text("https://your-mac/api/aiden/v1") },
singleLine = true,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
Expand All @@ -348,25 +351,12 @@ fun AidenPairingScreen(
}
}
},
enabled = manualCode.replace("-", "").length == 20 && !isPairing,
enabled = manualCode.replace("-", "").length == 20 && endpointUrl.isNotBlank() && !isPairing,
colors = ButtonDefaults.buttonColors(containerColor = palette.accent),
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.tactilePress {
scope.launch {
isPairing = true
errorMessage = null
try {
coordinator.pairWithManualCode(manualCode, endpointUrl)
onDismiss()
} catch (e: Exception) {
errorMessage = e.message ?: "Failed to pair with setup code"
} finally {
isPairing = false
}
}
}
.tactilePress()
) {
if (isPairing) {
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp))
Expand Down Expand Up @@ -399,7 +389,7 @@ fun AidenPairingScreen(
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.tactilePress { handleScannedQRCode(qrJsonInput) }
.tactilePress()
) {
if (isPairing) {
CircularProgressIndicator(color = Color.White, modifier = Modifier.size(20.dp))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.QrCodeScanner
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
Expand All @@ -29,7 +34,7 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
Expand All @@ -42,6 +47,7 @@ import com.google.mlkit.vision.common.InputImage
import sbtbiswas.AidenOnTheGo.ui.theme.AidenTheme
import sbtbiswas.AidenOnTheGo.ui.theme.tactilePress
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean

/**
* High-fidelity CameraX and MLKit QR Code Scanner with Viewfinder Overlay.
Expand Down Expand Up @@ -102,7 +108,7 @@ fun AidenQRCodeScanner(
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.tactilePress { permissionLauncher.launch(Manifest.permission.CAMERA) }
.tactilePress()
) {
Icon(Icons.Default.Videocam, contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(modifier = Modifier.width(8.dp))
Expand Down Expand Up @@ -131,7 +137,8 @@ private fun CameraPreview(
) {
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
var deliveredCode by remember { mutableStateOf(false) }
val closed = remember { AtomicBoolean(false) }
val delivered = remember { AtomicBoolean(false) }

val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
val scanner = remember {
Expand All @@ -143,13 +150,28 @@ private fun CameraPreview(

val cameraExecutor = remember { Executors.newSingleThreadExecutor() }

DisposableEffect(lifecycleOwner) {
closed.set(false)
onDispose {
closed.set(true)
cameraExecutor.shutdownNow()
scanner.close()
runCatching {
if (cameraProviderFuture.isDone) {
cameraProviderFuture.get().unbindAll()
}
}
}
}

AndroidView(
factory = { ctx ->
val previewView = PreviewView(ctx).apply {
scaleType = PreviewView.ScaleType.FILL_CENTER
}

cameraProviderFuture.addListener({
if (closed.get()) return@addListener
val cameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder().build().also {
it.setSurfaceProvider(previewView.surfaceProvider)
Expand All @@ -161,16 +183,15 @@ private fun CameraPreview(

imageAnalysis.setAnalyzer(cameraExecutor) { imageProxy ->
val mediaImage = imageProxy.image
if (mediaImage != null && !deliveredCode) {
if (mediaImage != null && !closed.get() && !delivered.get()) {
val image = InputImage.fromMediaImage(
mediaImage,
imageProxy.imageInfo.rotationDegrees
)
scanner.process(image)
.addOnSuccessListener { barcodes ->
val qr = barcodes.firstOrNull()?.rawValue
if (qr != null && !deliveredCode) {
deliveredCode = true
if (qr != null && !closed.get() && delivered.compareAndSet(false, true)) {
onCodeScanned(qr)
}
}
Expand All @@ -183,6 +204,10 @@ private fun CameraPreview(
}

try {
if (closed.get()) {
cameraProvider.unbindAll()
return@addListener
}
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
lifecycleOwner,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import sbtbiswas.AidenOnTheGo.AidenAppVersion
import sbtbiswas.AidenOnTheGo.intents.AidenIntentCatalogStore
import sbtbiswas.AidenOnTheGo.intents.AidenIntentInstallationRecord
import sbtbiswas.AidenOnTheGo.intents.AidenIntentWorkspaceRecord
Expand Down Expand Up @@ -303,7 +304,8 @@ class AidenRemoteCoordinator(
val exchange = AidenRemoteClient.pair(
payload = payload,
deviceName = deviceName,
deviceType = AidenDeviceType.ANDROID_PHONE
deviceType = AidenDeviceType.ANDROID_PHONE,
clientVersion = AidenAppVersion.NAME
)
val installation = installationStore.addInstallation(exchange, payload.trust)
refreshClient()
Expand All @@ -315,7 +317,8 @@ class AidenRemoteCoordinator(
manualCode = code,
endpoint = endpoint,
deviceName = deviceName,
deviceType = AidenDeviceType.ANDROID_PHONE
deviceType = AidenDeviceType.ANDROID_PHONE,
clientVersion = AidenAppVersion.NAME
)
val installation = installationStore.addInstallation(result.exchange, result.payload.trust)
refreshClient()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.ResponseBody
import sbtbiswas.AidenOnTheGo.AidenAppVersion
import sbtbiswas.AidenOnTheGo.models.*
import sbtbiswas.AidenOnTheGo.protocol.*
import sbtbiswas.AidenOnTheGo.diagnostics.AidenDiagnosticArea
Expand Down Expand Up @@ -141,7 +142,7 @@ class AidenRemoteClient(
payload: AidenPairingPayload,
deviceName: String,
deviceType: AidenDeviceType,
clientVersion: String = "0.1.0",
clientVersion: String = AidenAppVersion.NAME,
acceptsBotCapabilities: Boolean = true,
customOkHttpClient: OkHttpClient? = null
): AidenPairingExchange = withContext(Dispatchers.IO) {
Expand Down Expand Up @@ -222,7 +223,7 @@ class AidenRemoteClient(
endpoint: String,
deviceName: String,
deviceType: AidenDeviceType,
clientVersion: String = "0.1.0",
clientVersion: String = AidenAppVersion.NAME,
acceptsBotCapabilities: Boolean = true,
customOkHttpClient: OkHttpClient? = null
): PairResult = withContext(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import kotlin.math.pow

Expand Down Expand Up @@ -68,12 +69,19 @@ fun Modifier.tactilePress(
this
.scale(scale)
.pointerInput(onClick) {
awaitEachGesture {
awaitFirstDown().also { isPressed = true }
val up = waitForUpOrCancellation()
isPressed = false
if (up != null && onClick != null) {
onClick()
if (onClick == null) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Final)
isPressed = event.changes.any { it.pressed }
}
}
} else {
awaitEachGesture {
awaitFirstDown().also { isPressed = true }
val up = waitForUpOrCancellation()
isPressed = false
if (up != null) onClick()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package sbtbiswas.AidenOnTheGo

import org.junit.Assert.assertEquals
import org.junit.Test

class AidenAppVersionTest {
@Test
fun testClientVersionMatchesGradleVersionName() {
assertEquals(BuildConfig.VERSION_NAME, AidenAppVersion.NAME)
}
}
4 changes: 3 additions & 1 deletion docs/aiden-on-the-go-remote-access.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Aiden On The Go remote access

Aiden Agent can expose a small authenticated API to Aiden On The Go on iPhone and iPad. Remote Access is off by default. Aiden must remain running on the Mac, although its window may be closed.
Aiden Agent can expose a small authenticated API to Aiden On The Go on iPhone, iPad, and Android. Remote Access is off by default. Aiden must remain running on the Mac, although its window may be closed.

iPhone and iPad remain Internal TestFlight-only. Android is a debug-APK validation client; public Android availability is planned for October 2026.

## Local Network setup

Expand Down
Loading
Loading