diff --git a/apps/mobile/android/.gitignore b/apps/mobile/android/.gitignore
index 48354a3..5c0d4cc 100644
--- a/apps/mobile/android/.gitignore
+++ b/apps/mobile/android/.gitignore
@@ -89,13 +89,3 @@ lint/tmp/
# Android Profiling
*.hprof
-# Cordova plugins for Capacitor
-capacitor-cordova-android-plugins
-
-# Copied web assets
-app/src/main/assets/public
-
-# Generated Config files
-app/src/main/assets/capacitor.config.json
-app/src/main/assets/capacitor.plugins.json
-app/src/main/res/xml/config.xml
diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle
index 7da1198..49c0294 100644
--- a/apps/mobile/android/app/build.gradle
+++ b/apps/mobile/android/app/build.gradle
@@ -1,54 +1,60 @@
-apply plugin: 'com.android.application'
+plugins {
+ id 'com.android.application'
+ id 'org.jetbrains.kotlin.android'
+ id 'org.jetbrains.kotlin.plugin.compose'
+}
android {
- namespace "cloud.molberg.hermesmobile"
- compileSdk rootProject.ext.compileSdkVersion
+ namespace 'cloud.molberg.hermesmobile'
+ compileSdk 35
+
defaultConfig {
- applicationId "cloud.molberg.hermesmobile"
- minSdkVersion rootProject.ext.minSdkVersion
- targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 1
- versionName "1.0"
- testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
- aaptOptions {
- // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
- // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
- ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
- }
+ applicationId 'cloud.molberg.hermesmobile'
+ minSdk 23
+ targetSdk 35
+ versionCode 2
+ versionName '0.2.0-native'
+ testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
+
buildTypes {
release {
minifyEnabled false
- proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
-}
-repositories {
- flatDir{
- dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ buildFeatures {
+ compose true
}
}
dependencies {
- implementation fileTree(include: ['*.jar'], dir: 'libs')
- implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
- implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
- implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
- implementation project(':capacitor-android')
- testImplementation "junit:junit:$junitVersion"
- androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
- androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
- implementation project(':capacitor-cordova-android-plugins')
-}
+ implementation platform('androidx.compose:compose-bom:2025.01.01')
+ implementation 'androidx.activity:activity-compose:1.10.0'
+ implementation 'androidx.compose.animation:animation'
+ implementation 'androidx.compose.foundation:foundation'
+ implementation 'androidx.compose.material3:material3'
+ implementation 'androidx.compose.material:material-icons-extended'
+ implementation 'androidx.compose.ui:ui'
+ implementation 'androidx.compose.ui:ui-tooling-preview'
+ implementation 'androidx.core:core-ktx:1.15.0'
+ implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.7'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0'
+ implementation 'com.squareup.okhttp3:okhttp:4.12.0'
+ implementation 'org.json:json:20240303'
-apply from: 'capacitor.build.gradle'
-
-try {
- def servicesJSON = file('google-services.json')
- if (servicesJSON.text) {
- apply plugin: 'com.google.gms.google-services'
- }
-} catch(Exception e) {
- logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
+ debugImplementation 'androidx.compose.ui:ui-tooling'
+ testImplementation 'junit:junit:4.13.2'
+ androidTestImplementation 'androidx.test.ext:junit:1.2.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}
diff --git a/apps/mobile/android/app/capacitor.build.gradle b/apps/mobile/android/app/capacitor.build.gradle
deleted file mode 100644
index 23c73a9..0000000
--- a/apps/mobile/android/app/capacitor.build.gradle
+++ /dev/null
@@ -1,19 +0,0 @@
-// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
-
-android {
- compileOptions {
- sourceCompatibility JavaVersion.VERSION_21
- targetCompatibility JavaVersion.VERSION_21
- }
-}
-
-apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
-dependencies {
- implementation project(':capacitor-haptics')
-
-}
-
-
-if (hasProperty('postBuildExtras')) {
- postBuildExtras()
-}
diff --git a/apps/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/apps/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
deleted file mode 100644
index f2c2217..0000000
--- a/apps/mobile/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.getcapacitor.myapp;
-
-import static org.junit.Assert.*;
-
-import android.content.Context;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.platform.app.InstrumentationRegistry;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
-
- @Test
- public void useAppContext() throws Exception {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
-
- assertEquals("com.getcapacitor.app", appContext.getPackageName());
- }
-}
diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml
index 340e7df..7b3c48a 100644
--- a/apps/mobile/android/app/src/main/AndroidManifest.xml
+++ b/apps/mobile/android/app/src/main/AndroidManifest.xml
@@ -1,41 +1,23 @@
+
-
-
+ android:exported="true"
+ android:screenOrientation="portrait">
-
-
-
-
-
-
-
-
-
diff --git a/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.java b/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.java
deleted file mode 100644
index 85740ec..0000000
--- a/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package cloud.molberg.hermesmobile;
-
-import com.getcapacitor.BridgeActivity;
-
-public class MainActivity extends BridgeActivity {}
diff --git a/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.kt b/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.kt
new file mode 100644
index 0000000..467eb4e
--- /dev/null
+++ b/apps/mobile/android/app/src/main/java/cloud/molberg/hermesmobile/MainActivity.kt
@@ -0,0 +1,613 @@
+package cloud.molberg.hermesmobile
+
+import android.app.Application
+import android.content.Context
+import android.os.Bundle
+import android.os.VibrationEffect
+import android.os.Vibrator
+import android.os.VibratorManager
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.ContentTransform
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.asPaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.statusBarsPadding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.ChatBubble
+import androidx.compose.material.icons.filled.CheckCircle
+import androidx.compose.material.icons.filled.Favorite
+import androidx.compose.material.icons.filled.Folder
+import androidx.compose.material.icons.filled.Key
+import androidx.compose.material.icons.filled.PlayArrow
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Save
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.Terminal
+import androidx.compose.material.icons.filled.Wifi
+import androidx.compose.material.icons.filled.WifiOff
+import androidx.compose.material.icons.outlined.Article
+import androidx.compose.material.icons.outlined.Bolt
+import androidx.compose.material.icons.outlined.Diamond
+import androidx.compose.material.icons.outlined.LocalFireDepartment
+import androidx.compose.material.icons.outlined.EmojiEvents
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.NavigationBar
+import androidx.compose.material3.NavigationBarItem
+import androidx.compose.material3.NavigationBarItemDefaults
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextFieldDefaults
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
+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
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.json.JSONArray
+import org.json.JSONObject
+import java.net.URLEncoder
+import java.util.concurrent.TimeUnit
+
+enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) {
+ Quest("Quest", Icons.Filled.ChatBubble, "Quest", "Hermes training path"),
+ Files("Files", Icons.Filled.Folder, "Backpack", "Workspace loot"),
+ Terminal("Terminal", Icons.Filled.Terminal, "Power move", "Run a command"),
+ Status("Status", Icons.Outlined.EmojiEvents, "League", "Companion stats"),
+ Settings("Settings", Icons.Filled.Settings, "Nest", "Connection setup")
+}
+
+data class FileEntry(val name: String, val path: String, val type: String, val size: Long)
+data class ChatMessage(val role: String, val content: String)
+
+data class HealthState(
+ val reachable: Boolean = false,
+ val status: String = "Tap refresh to check companion status.",
+ val hermesCli: String = "Unknown / unavailable",
+ val workspace: String = "Unknown",
+ val uptime: String = "—",
+ val mode: String = "—"
+)
+
+class CompanionApi(private val context: Context) {
+ private val prefs = context.getSharedPreferences("companion", Context.MODE_PRIVATE)
+ private val http = OkHttpClient.Builder()
+ .connectTimeout(10, TimeUnit.SECONDS)
+ .readTimeout(120, TimeUnit.SECONDS)
+ .build()
+
+ var baseUrl: String
+ get() = prefs.getString("baseUrl", "http://10.0.2.2:8787") ?: "http://10.0.2.2:8787"
+ set(value) { prefs.edit().putString("baseUrl", value.trim().ifBlank { "http://10.0.2.2:8787" }).apply() }
+
+ var accessKey: String
+ get() = prefs.getString("accessKey", "") ?: ""
+ set(value) { prefs.edit().putString("accessKey", value.trim()).apply() }
+
+ private fun endpoint(path: String): String = baseUrl.trimEnd('/') + path
+
+ private fun request(path: String, method: String = "GET", body: JSONObject? = null): JSONObject {
+ val mediaType = "application/json; charset=utf-8".toMediaType()
+ val builder = Request.Builder()
+ .url(endpoint(path))
+ .header("Authorization", "Bearer $accessKey")
+ .header("Accept", "application/json")
+ val request = when (method) {
+ "POST" -> builder.post((body ?: JSONObject()).toString().toRequestBody(mediaType)).build()
+ "PUT" -> builder.put((body ?: JSONObject()).toString().toRequestBody(mediaType)).build()
+ else -> builder.get().build()
+ }
+ http.newCall(request).execute().use { response ->
+ val text = response.body?.string().orEmpty()
+ if (!response.isSuccessful) throw IllegalStateException(text.ifBlank { "HTTP ${response.code}" })
+ return if (text.isBlank()) JSONObject() else JSONObject(text)
+ }
+ }
+
+ suspend fun health(): HealthState = withContext(Dispatchers.IO) {
+ val json = request("/api/health")
+ val checks = json.optJSONObject("checks")?.optJSONObject("hermes")
+ val config = json.optJSONObject("config")
+ HealthState(
+ reachable = true,
+ status = "Companion is reachable.",
+ hermesCli = if (checks?.optBoolean("cliAvailable") == true) "Found at ${checks.optString("cliPath")}" else "Unknown / unavailable",
+ workspace = config?.optString("workspaceRoot") ?: "Unknown",
+ uptime = "${json.optDouble("uptimeSeconds", 0.0).toInt()}s",
+ mode = json.optString("mode", "—")
+ )
+ }
+
+ suspend fun validate(): String = withContext(Dispatchers.IO) {
+ request("/api/auth/validate").optString("message", "Connection works.")
+ }
+
+ suspend fun chat(prompt: String): String = withContext(Dispatchers.IO) {
+ request("/api/chat", "POST", JSONObject().put("prompt", prompt)).optString("reply", "")
+ }
+
+ suspend fun listFiles(path: String): Pair> = withContext(Dispatchers.IO) {
+ val encoded = URLEncoder.encode(path, "UTF-8")
+ val json = request("/api/files?path=$encoded")
+ val entries = json.optJSONArray("entries") ?: JSONArray()
+ json.optString("path", path) to List(entries.length()) { i ->
+ val item = entries.getJSONObject(i)
+ FileEntry(
+ name = item.optString("name"),
+ path = item.optString("path"),
+ type = item.optString("type"),
+ size = item.optLong("size", 0)
+ )
+ }
+ }
+
+ suspend fun readFile(path: String): Pair = withContext(Dispatchers.IO) {
+ val encoded = URLEncoder.encode(path, "UTF-8")
+ val json = request("/api/files/read?path=$encoded")
+ json.optString("path", path) to json.optString("content", "")
+ }
+
+ suspend fun writeFile(path: String, content: String): String = withContext(Dispatchers.IO) {
+ val json = request("/api/files/write", "PUT", JSONObject().put("path", path).put("content", content))
+ "Saved ${json.optLong("bytesWritten")} bytes"
+ }
+
+ suspend fun runCommand(command: String, cwd: String): String = withContext(Dispatchers.IO) {
+ val json = request("/api/terminal/run", "POST", JSONObject().put("command", command).put("cwd", cwd))
+ listOf(
+ "$ ${json.optString("command", command)}",
+ json.optString("stdout"),
+ json.optString("stderr").takeIf { it.isNotBlank() }?.let { "stderr:\n$it" }.orEmpty(),
+ "exitCode=${json.opt("exitCode")} timedOut=${json.optBoolean("timedOut")}"
+ ).filter { it.isNotBlank() }.joinToString("\n")
+ }
+}
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ enableEdgeToEdge()
+ setContent { HermesNativeApp() }
+ }
+}
+
+@Composable
+fun HermesNativeApp() {
+ val context = LocalContext.current
+ val api = remember { CompanionApi(context.applicationContext) }
+ var screen by remember { mutableStateOf(Screen.Quest) }
+ var online by remember { mutableStateOf(false) }
+
+ LaunchedEffect(screen) {
+ runCatching { api.health() }.onSuccess { online = true }.onFailure { online = false }
+ }
+
+ MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy(background = Cream, surface = Paper)) {
+ Scaffold(
+ modifier = Modifier.fillMaxSize(),
+ containerColor = Cream,
+ topBar = { NativeTopBar(screen, online) },
+ bottomBar = {
+ NativeBottomBar(
+ current = screen,
+ onSelect = {
+ if (it != screen) vibrate(context)
+ screen = it
+ }
+ )
+ }
+ ) { padding ->
+ AnimatedContent(
+ targetState = screen,
+ transitionSpec = { slideFor(initialState.ordinal, targetState.ordinal) },
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(padding)
+ .imePadding(),
+ label = "screen-slide"
+ ) { active ->
+ when (active) {
+ Screen.Quest -> QuestScreen(api)
+ Screen.Files -> FilesScreen(api)
+ Screen.Terminal -> TerminalScreen(api)
+ Screen.Status -> StatusScreen(api)
+ Screen.Settings -> SettingsScreen(api)
+ }
+ }
+ }
+ }
+}
+
+fun slideFor(from: Int, to: Int): ContentTransform {
+ val direction = if (to > from) 1 else -1
+ return slideInHorizontally(animationSpec = tween(260)) { width -> direction * width } togetherWith
+ slideOutHorizontally(animationSpec = tween(260)) { width -> -direction * width }
+}
+
+fun vibrate(context: Context) {
+ val vibrator = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
+ val manager = context.getSystemService(Application.VIBRATOR_MANAGER_SERVICE) as VibratorManager
+ manager.defaultVibrator
+ } else {
+ @Suppress("DEPRECATION") context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
+ }
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK))
+ else @Suppress("DEPRECATION") vibrator.vibrate(20)
+}
+
+@Composable
+fun NativeTopBar(screen: Screen, online: Boolean) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .statusBarsPadding()
+ .padding(horizontal = 16.dp, vertical = 10.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ RaisedCircle(color = Green, shadow = GreenDark, content = "⚡")
+ Column(Modifier.weight(1f)) {
+ TinyLabel(screen.kicker)
+ Text(screen.title, fontSize = 31.sp, fontWeight = FontWeight.Black, color = Ink, lineHeight = 31.sp)
+ }
+ StatChip(Icons.Outlined.LocalFireDepartment, "7", Orange)
+ StatChip(Icons.Outlined.Diamond, "550", Blue)
+ StatChip(Icons.Filled.Favorite, "3", Red)
+ }
+ RaisedCard(
+ color = if (online) PaleGreen else PaleOrange,
+ shadow = if (online) GreenShadow else OrangeShadow,
+ border = if (online) GreenBorder else OrangeBorder,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Icon(if (online) Icons.Filled.Wifi else Icons.Filled.WifiOff, contentDescription = null, tint = if (online) GreenDark else Orange)
+ Text(if (online) "Companion online — ready for quests" else "Companion offline — check Settings", color = if (online) GreenDark else Orange, fontWeight = FontWeight.Black)
+ }
+ }
+ }
+}
+
+@Composable
+fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
+ Surface(shadowElevation = 14.dp, color = Paper, tonalElevation = 0.dp) {
+ NavigationBar(
+ modifier = Modifier
+ .fillMaxWidth()
+ .navigationBarsPadding()
+ .height(82.dp),
+ containerColor = Paper,
+ tonalElevation = 0.dp
+ ) {
+ Screen.entries.forEach { item ->
+ NavigationBarItem(
+ selected = current == item,
+ onClick = { onSelect(item) },
+ icon = { Icon(item.icon, contentDescription = item.label) },
+ label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) },
+ colors = NavigationBarItemDefaults.colors(
+ selectedIconColor = GreenDark,
+ selectedTextColor = GreenDark,
+ indicatorColor = PaleGreen,
+ unselectedIconColor = Muted,
+ unselectedTextColor = Muted
+ )
+ )
+ }
+ }
+ }
+}
+
+@Composable
+fun QuestScreen(api: CompanionApi) {
+ val scope = remember { CoroutineScope(Dispatchers.Main) }
+ var prompt by remember { mutableStateOf("") }
+ var busy by remember { mutableStateOf(false) }
+ var error by remember { mutableStateOf("") }
+ val messages = remember { mutableStateListOf() }
+
+ ScreenList {
+ HeroCard(icon = Icons.Filled.ChatBubble, title = "Give Hermes one clean mission.", label = "Daily quest", text = "Short, direct prompts work best. The native app calls the local companion server.")
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
+ PathPill(Icons.Filled.CheckCircle, "Check", true)
+ PathPill(Icons.Outlined.Bolt, "Ask", true, current = true)
+ PathPill(Icons.Filled.ChatBubble, "Review", false)
+ }
+ if (messages.isEmpty()) EmptyLesson()
+ messages.forEach { Bubble(it) }
+ if (error.isNotBlank()) ErrorCard(error)
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ AppTextField(value = prompt, onValue = { prompt = it }, placeholder = "What should Hermes do?", minLines = 3)
+ PrimaryButton(text = if (busy) "Working…" else "Start quest", icon = Icons.Filled.PlayArrow, enabled = !busy && prompt.isNotBlank()) {
+ val p = prompt.trim()
+ prompt = ""
+ messages.add(ChatMessage("You", p))
+ busy = true
+ error = ""
+ scope.launch {
+ runCatching { api.chat(p) }
+ .onSuccess { messages.add(ChatMessage("Hermes", it)) }
+ .onFailure { error = it.message ?: "Chat failed" }
+ busy = false
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun FilesScreen(api: CompanionApi) {
+ val scope = remember { CoroutineScope(Dispatchers.Main) }
+ var path by remember { mutableStateOf(".") }
+ var entries by remember { mutableStateOf>(emptyList()) }
+ var activePath by remember { mutableStateOf("") }
+ var draft by remember { mutableStateOf("") }
+ var status by remember { mutableStateOf("") }
+
+ fun load(next: String = path) = scope.launch {
+ runCatching { api.listFiles(next) }
+ .onSuccess { (p, list) -> path = p; entries = list; activePath = ""; draft = ""; status = "" }
+ .onFailure { status = it.message ?: "Unable to list files" }
+ }
+ LaunchedEffect(Unit) { load(".") }
+
+ ScreenList {
+ CompactQuest(title = "Workspace files", label = "Backpack", text = "Tap folders to go deeper. Files open in a native editor.", icon = Icons.Filled.Folder) { load() }
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ TextButton(onClick = { load(parentPath(path)) }, enabled = path != ".") { Text("Up", fontWeight = FontWeight.Black) }
+ Text(path, Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis, color = Muted, fontWeight = FontWeight.Black)
+ }
+ entries.forEach { entry ->
+ FileRow(entry) {
+ if (entry.type == "directory") load(entry.path) else scope.launch {
+ runCatching { api.readFile(entry.path) }
+ .onSuccess { (p, c) -> activePath = p; draft = c }
+ .onFailure { status = it.message ?: "Unable to read file" }
+ }
+ }
+ }
+ }
+ }
+ if (activePath.isNotBlank()) RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ TinyLabel("Editing")
+ Text(activePath, color = Ink, fontWeight = FontWeight.Black, maxLines = 2, overflow = TextOverflow.Ellipsis)
+ AppTextField(draft, { draft = it }, "File content", minLines = 10)
+ PrimaryButton("Save", Icons.Filled.Save) {
+ scope.launch { runCatching { api.writeFile(activePath, draft) }.onSuccess { status = it }.onFailure { status = it.message ?: "Save failed" } }
+ }
+ }
+ }
+ if (status.isNotBlank()) ErrorCard(status)
+ }
+}
+
+@Composable
+fun TerminalScreen(api: CompanionApi) {
+ val scope = remember { CoroutineScope(Dispatchers.Main) }
+ var cwd by remember { mutableStateOf(".") }
+ var command by remember { mutableStateOf("pwd && ls") }
+ var output by remember { mutableStateOf("Command output appears here.") }
+ var busy by remember { mutableStateOf(false) }
+
+ ScreenList {
+ HeroCard(icon = Icons.Filled.Terminal, title = "Run one safe command.", label = "Power move", text = "Commands execute through the companion server inside the configured workspace.", orbColor = Blue, orbShadow = BlueDark)
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ FieldLabel("Working directory")
+ AppTextField(cwd, { cwd = it }, ".")
+ FieldLabel("Command")
+ AppTextField(command, { command = it }, "pwd && ls", minLines = 4)
+ PrimaryButton(if (busy) "Running…" else "Run command", Icons.Filled.PlayArrow, enabled = !busy && command.isNotBlank()) {
+ busy = true
+ scope.launch {
+ runCatching { api.runCommand(command, cwd) }.onSuccess { output = it }.onFailure { output = it.message ?: "Command failed" }
+ busy = false
+ }
+ }
+ }
+ }
+ CodeCard(output)
+ }
+}
+
+@Composable
+fun StatusScreen(api: CompanionApi) {
+ val scope = remember { CoroutineScope(Dispatchers.Main) }
+ var health by remember { mutableStateOf(HealthState()) }
+ fun refresh() = scope.launch { health = runCatching { api.health() }.getOrElse { HealthState(false, it.message ?: "Health check failed") } }
+
+ ScreenList {
+ CompactQuest(title = "Companion status", label = "League board", text = health.status, icon = Icons.Outlined.EmojiEvents, orbColor = Gold, orbShadow = GoldDark) { refresh() }
+ Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ Metric("Hermes", if (health.hermesCli.startsWith("Found")) "Ready" else "—", Modifier.weight(1f))
+ Metric("Uptime", health.uptime, Modifier.weight(1f))
+ Metric("Mode", health.mode, Modifier.weight(1f))
+ }
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
+ FieldLabel("Companion URL")
+ Text(api.baseUrl, color = Ink, fontWeight = FontWeight.Black)
+ FieldLabel("Hermes CLI")
+ Text(health.hermesCli, color = Ink, fontWeight = FontWeight.Black)
+ FieldLabel("Workspace")
+ Text(health.workspace, color = Ink, fontWeight = FontWeight.Black)
+ }
+ }
+ }
+}
+
+@Composable
+fun SettingsScreen(api: CompanionApi) {
+ val scope = remember { CoroutineScope(Dispatchers.Main) }
+ var baseUrl by remember { mutableStateOf(api.baseUrl) }
+ var accessKey by remember { mutableStateOf(api.accessKey) }
+ var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") }
+
+ ScreenList {
+ CompactQuest(title = "Pair your companion", label = "Nest setup", text = status, icon = Icons.Filled.Settings, orbColor = Green, orbShadow = GreenDark) {}
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
+ FieldLabel("Companion server URL")
+ AppTextField(baseUrl, { baseUrl = it }, "http://10.0.2.2:8787", keyboardType = KeyboardType.Uri)
+ FieldLabel("Access key")
+ AppTextField(accessKey, { accessKey = it }, "hm_…", password = true)
+ Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ SecondaryButton("Save", Icons.Filled.Save, Modifier.weight(1f)) { api.baseUrl = baseUrl; api.accessKey = accessKey; status = "Settings saved." }
+ PrimaryButton("Test", Icons.Filled.Wifi, Modifier.weight(1f)) {
+ api.baseUrl = baseUrl; api.accessKey = accessKey; status = "Testing…"
+ scope.launch { status = runCatching { api.validate() }.getOrElse { it.message ?: "Connection failed" } }
+ }
+ }
+ }
+ }
+ }
+}
+
+fun parentPath(path: String): String = path.split('/').filter { it.isNotBlank() }.dropLast(1).joinToString("/").ifBlank { "." }
+
+@Composable
+fun ScreenList(content: @Composable ColumnScope.() -> Unit) {
+ LazyColumn(
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 16.dp, vertical = 8.dp),
+ verticalArrangement = Arrangement.spacedBy(14.dp),
+ content = { item { Column(verticalArrangement = Arrangement.spacedBy(14.dp), content = content) } }
+ )
+}
+
+@Composable
+fun RaisedCard(modifier: Modifier = Modifier, color: Color = Paper, shadow: Color = NeutralShadow, border: Color = NeutralBorder, content: @Composable () -> Unit) {
+ Box(modifier.padding(bottom = 5.dp)) {
+ Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(22.dp)).background(shadow))
+ Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(22.dp), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(2.dp, border), elevation = CardDefaults.cardElevation(0.dp)) { content() }
+ }
+}
+
+@Composable
+fun HeroCard(icon: ImageVector, title: String, label: String, text: String, orbColor: Color = Green, orbShadow: Color = GreenDark) {
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Row(Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.Top) {
+ RaisedIcon(icon, orbColor, orbShadow)
+ Column(Modifier.weight(1f)) { TinyLabel(label); Text(title, color = Ink, fontWeight = FontWeight.Black, fontSize = 28.sp, lineHeight = 29.sp); Text(text, color = Muted, fontWeight = FontWeight.ExtraBold, lineHeight = 18.sp) }
+ }
+ }
+}
+
+@Composable
+fun CompactQuest(title: String, label: String, text: String, icon: ImageVector, orbColor: Color = Green, orbShadow: Color = GreenDark, onRefresh: () -> Unit) {
+ RaisedCard(Modifier.fillMaxWidth()) {
+ Row(Modifier.padding(14.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
+ RaisedIcon(icon, orbColor, orbShadow, size = 58)
+ Column(Modifier.weight(1f)) { TinyLabel(label); Text(title, color = Ink, fontWeight = FontWeight.Black, fontSize = 24.sp); Text(text, color = Muted, fontWeight = FontWeight.ExtraBold, lineHeight = 17.sp) }
+ if (onRefresh !== {}) RoundButton(Icons.Filled.Refresh, onRefresh)
+ }
+ }
+}
+
+@Composable fun RaisedIcon(icon: ImageVector, color: Color, shadow: Color, size: Int = 66) { Box(Modifier.size(size.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(CircleShape).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(CircleShape).background(color), contentAlignment = Alignment.Center) { Icon(icon, null, tint = Color.White, modifier = Modifier.size((size / 2).dp)) } } }
+@Composable fun RaisedCircle(color: Color, shadow: Color, content: String) { Box(Modifier.size(54.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(18.dp)).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(RoundedCornerShape(18.dp)).background(color), contentAlignment = Alignment.Center) { Text(content, fontSize = 24.sp) } } }
+@Composable fun StatChip(icon: ImageVector, value: String, color: Color) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.clip(RoundedCornerShape(99.dp)).background(Paper).padding(horizontal = 7.dp, vertical = 3.dp)) { Icon(icon, null, tint = color, modifier = Modifier.size(15.dp)); Text(value, color = color, fontWeight = FontWeight.Black, fontSize = 12.sp) } }
+@Composable fun TinyLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 11.sp, fontWeight = FontWeight.Black, letterSpacing = 0.7.sp) }
+@Composable fun FieldLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.Black) }
+@Composable fun PathPill(icon: ImageVector, text: String, done: Boolean, current: Boolean = false) { Row(Modifier.clip(RoundedCornerShape(99.dp)).background(if (current) Green else if (done) PaleGreen else Surface).padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Icon(icon, null, tint = if (current) Color.White else GreenDark, modifier = Modifier.size(18.dp)); Text(text, color = if (current) Color.White else GreenDark, fontWeight = FontWeight.Black, fontSize = 13.sp) } }
+@Composable fun EmptyLesson() { RaisedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(18.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text("🪽", fontSize = 42.sp); Text("No mission yet", color = Ink, fontWeight = FontWeight.Black, fontSize = 20.sp); Text("Try: check PM2 status or summarize logs.", color = Muted, fontWeight = FontWeight.ExtraBold) } } }
+@Composable fun Bubble(msg: ChatMessage) { RaisedCard(Modifier.fillMaxWidth().padding(start = if (msg.role == "You") 28.dp else 0.dp, end = if (msg.role == "You") 0.dp else 28.dp), color = if (msg.role == "You") PaleGreen else PaleGold) { Column(Modifier.padding(12.dp)) { TinyLabel(msg.role); Text(msg.content, color = Ink, fontWeight = FontWeight.ExtraBold, lineHeight = 19.sp) } } }
+@Composable fun ErrorCard(text: String) { RaisedCard(Modifier.fillMaxWidth(), color = PaleRed, shadow = RedShadow, border = RedBorder) { Text(text, Modifier.padding(12.dp), color = Red, fontWeight = FontWeight.Black) } }
+@Composable fun CodeCard(text: String) { RaisedCard(Modifier.fillMaxWidth()) { Text(text, Modifier.padding(14.dp), color = BlueDark, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontSize = 13.sp, lineHeight = 18.sp) } }
+@Composable fun FileRow(entry: FileEntry, onClick: () -> Unit) { RaisedCard(Modifier.fillMaxWidth()) { Row(Modifier.clickable(onClick = onClick).padding(10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Box(Modifier.size(42.dp).clip(RoundedCornerShape(14.dp)).background(if (entry.type == "directory") PaleBlue else PaleGold), contentAlignment = Alignment.Center) { Icon(if (entry.type == "directory") Icons.Filled.Folder else Icons.Outlined.Article, null, tint = if (entry.type == "directory") Blue else GoldDark) }; Text(entry.name, Modifier.weight(1f), color = Ink, fontWeight = FontWeight.Black, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(if (entry.type == "file") "${entry.size} B" else "dir", color = Muted, fontWeight = FontWeight.Black, fontSize = 12.sp) } } }
+@Composable fun Metric(label: String, value: String, modifier: Modifier = Modifier) { RaisedCard(modifier) { Column(Modifier.padding(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text(value, color = GreenDark, fontWeight = FontWeight.Black, fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(label.uppercase(), color = Muted, fontWeight = FontWeight.Black, fontSize = 10.sp) } } }
+@Composable fun AppTextField(value: String, onValue: (String) -> Unit, placeholder: String, minLines: Int = 1, keyboardType: KeyboardType = KeyboardType.Text, password: Boolean = false) { OutlinedTextField(value = value, onValueChange = onValue, modifier = Modifier.fillMaxWidth(), placeholder = { Text(placeholder, color = Muted) }, minLines = minLines, keyboardOptions = KeyboardOptions(keyboardType = keyboardType), visualTransformation = if (password) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None, shape = RoundedCornerShape(16.dp), colors = TextFieldDefaults.colors(focusedContainerColor = Surface, unfocusedContainerColor = Surface, focusedIndicatorColor = Green, unfocusedIndicatorColor = NeutralBorder, cursorColor = GreenDark)) }
+@Composable fun PrimaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { Button(onClick = onClick, enabled = enabled, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Green, contentColor = Color.White, disabledContainerColor = NeutralBorder), elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } }
+@Composable fun SecondaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, onClick: () -> Unit) { Button(onClick = onClick, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Surface, contentColor = Ink), elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } }
+@Composable fun RoundButton(icon: ImageVector, onClick: () -> Unit) { Button(onClick = onClick, modifier = Modifier.size(50.dp), shape = RoundedCornerShape(16.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(0.dp), colors = ButtonDefaults.buttonColors(containerColor = Blue, contentColor = Color.White), elevation = ButtonDefaults.buttonElevation(defaultElevation = 5.dp, pressedElevation = 1.dp)) { Icon(icon, null) } }
+
+val Green = Color(0xFF58CC02)
+val GreenDark = Color(0xFF46A302)
+val GreenShadow = Color(0xFFB8E9A4)
+val GreenBorder = Color(0xFF8DDB6C)
+val PaleGreen = Color(0xFFE8F8DF)
+val Blue = Color(0xFF1CB0F6)
+val BlueDark = Color(0xFF1687C2)
+val PaleBlue = Color(0xFFE2F3FF)
+val Gold = Color(0xFFFFC800)
+val GoldDark = Color(0xFFC99500)
+val PaleGold = Color(0xFFFFF6D6)
+val Orange = Color(0xFFFF9600)
+val PaleOrange = Color(0xFFFFF0D8)
+val OrangeBorder = Color(0xFFE7BE82)
+val OrangeShadow = Color(0xFFE4C99D)
+val Red = Color(0xFFFF4B4B)
+val PaleRed = Color(0xFFFFE4E4)
+val RedBorder = Color(0xFFFFB8B8)
+val RedShadow = Color(0xFFE9B4B4)
+val Cream = Color(0xFFF2F7E8)
+val Paper = Color(0xFFFFFEF8)
+val Surface = Color(0xFFF3F8ED)
+val Ink = Color(0xFF243B2E)
+val Muted = Color(0xFF6D7C72)
+val NeutralBorder = Color(0xFFDCE8D5)
+val NeutralShadow = Color(0xFFC8D8C0)
diff --git a/apps/mobile/android/app/src/main/res/layout/activity_main.xml b/apps/mobile/android/app/src/main/res/layout/activity_main.xml
deleted file mode 100644
index b5ad138..0000000
--- a/apps/mobile/android/app/src/main/res/layout/activity_main.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..20f86bc
--- /dev/null
+++ b/apps/mobile/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,4 @@
+
+
+ #58CC02
+
diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/mobile/android/app/src/main/res/values/styles.xml
index be874e5..c471cc5 100644
--- a/apps/mobile/android/app/src/main/res/values/styles.xml
+++ b/apps/mobile/android/app/src/main/res/values/styles.xml
@@ -1,22 +1,9 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/apps/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/apps/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
deleted file mode 100644
index 0297327..0000000
--- a/apps/mobile/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package com.getcapacitor.myapp;
-
-import static org.junit.Assert.*;
-
-import org.junit.Test;
-
-/**
- * Example local unit test, which will execute on the development machine (host).
- *
- * @see Testing documentation
- */
-public class ExampleUnitTest {
-
- @Test
- public void addition_isCorrect() throws Exception {
- assertEquals(4, 2 + 2);
- }
-}
diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle
index f1b3b0e..bbf0e6a 100644
--- a/apps/mobile/android/build.gradle
+++ b/apps/mobile/android/build.gradle
@@ -1,29 +1,5 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
-
-buildscript {
-
- repositories {
- google()
- mavenCentral()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:8.7.2'
- classpath 'com.google.gms:google-services:4.4.2'
-
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
-}
-
-apply from: "variables.gradle"
-
-allprojects {
- repositories {
- google()
- mavenCentral()
- }
-}
-
-task clean(type: Delete) {
- delete rootProject.buildDir
+plugins {
+ id 'com.android.application' version '8.7.2' apply false
+ id 'org.jetbrains.kotlin.android' version '2.0.21' apply false
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.0.21' apply false
}
diff --git a/apps/mobile/android/capacitor.settings.gradle b/apps/mobile/android/capacitor.settings.gradle
deleted file mode 100644
index 30260dc..0000000
--- a/apps/mobile/android/capacitor.settings.gradle
+++ /dev/null
@@ -1,6 +0,0 @@
-// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
-include ':capacitor-android'
-project(':capacitor-android').projectDir = new File('../../../node_modules/@capacitor/android/capacitor')
-
-include ':capacitor-haptics'
-project(':capacitor-haptics').projectDir = new File('../../../node_modules/@capacitor/haptics/android')
diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle
index 3b4431d..fe9bf8d 100644
--- a/apps/mobile/android/settings.gradle
+++ b/apps/mobile/android/settings.gradle
@@ -1,5 +1,18 @@
-include ':app'
-include ':capacitor-cordova-android-plugins'
-project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
-apply from: 'capacitor.settings.gradle'
\ No newline at end of file
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = 'Hermes Mobile'
+include ':app'
diff --git a/apps/mobile/android/variables.gradle b/apps/mobile/android/variables.gradle
index 2c8e408..1b45dac 100644
--- a/apps/mobile/android/variables.gradle
+++ b/apps/mobile/android/variables.gradle
@@ -12,5 +12,4 @@ ext {
junitVersion = '4.13.2'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
- cordovaAndroidVersion = '10.1.1'
}
\ No newline at end of file