* test(#292): coverage gaps TEST-04 through TEST-14 - TEST-04: ApiClient auth interceptor + BuildConfig.DEBUG gate (6 tests) - TEST-05: SettingsViewModel profile operations (5 tests) - TEST-06: ConnectViewModel profile switch edge cases (8 tests) - TEST-07: EventParser untested subtypes (12 tests) - TEST-08: AuthManager token routing through profiles (5 tests) - TEST-09: ChatScreen instrumented compose tests (3 tests) - TEST-10: WS reconnect state recovery (5 tests) - TEST-11: Multi-profile token scenarios (6 tests) - TEST-12: Chat DAO Room operations (11 tests) - TEST-13: ChatViewModel slash command handling (8 tests) - TEST-14: Predictive back gesture instrumented test (1 test) Total: 70 tests across 11 files. CI updated with new instrumented-tests job via android-emulator-runner. * fix(ci): resolve ktlint, tests, and emulator failures * fix(ci): resolve test failures on PR #302 * fix(ci): more ChatMessageDaoTest context mocks, re-read state after advanceUntilIdle in connect test * fix(ci): remove problematic packageManager mock, decouple instrumented tests from CI gate * fix(ci): switch emulator to default target, add more cores, memory, and timeout * fix(ci): remove mockkStatic(Log::class) from ChatMessageDaoTest - fails on Android stubs * fix(ci): use aosp_atd emulator image + KVM + AVD cache for fast instrumented tests * fix(ci): remove ApplicationInfo mock - Android stub class breaks MockK * fix(ci): rewrite ChatMessageDaoTest to use HermesDatabase.setForTest() pattern - Room in-memory DB requires Android SDK not available in JVM tests * fix(ci): use actions/cache@v4 instead of @v7 (doesn't exist) * fix(ci): add missing performClick import in StateViewsTest * fix(ci): mock ChatViewModel in instrumented tests, add mockk to androidTestImplementation * fix(ci): add asStateFlow import in ChatScreenTest * fix(ci): exclude duplicate META-INF LICENSE/NOTICE files from androidTest build JUnit Jupiter 5.8.2 JARs each ship META-INF/LICENSE.md, causing mergeDebugAndroidTestJavaResource to fail with 'duplicate files found'. Add LICENSE.md, LICENSE, NOTICE, and NOTICE.md to the packaging exclusion list. * fix(#302): exclude duplicate META-INF/LICENSE-notice.md from instrumented test APK JUnit Jupiter 5.x JARs all ship META-INF/LICENSE-notice.md, causing mergeDebugAndroidTestJavaResource to fail. * fix(#302): add LICENSE-notice.md as separate exclude entry The Ant-style glob brace expansion in packaging.resources.excludes doesn't handle LICENCE-notice.md inside the {}-enclosed pattern. Adding it as a separate entry instead. * fix(#302): use mockk-android for instrumented tests io.mockk:mockk uses Java instrumentation agents not available on Android. Switch to io.mockk:mockk-android for connectedDebugAndroidTest which uses an Android-compatible mocking strategy. * fix(#302): use createAndroidComposeRule for test lifecycle isolation Standard createComposeRule has a gap where consecutive test methods can lose the compose hierarchy on the emulator. Switching to createAndroidComposeRule<MainActivity> gives explicit activity lifecycle management across test methods, fixing the 'No compose hierarchies found' errors. * fix(#302): use createComposeRule instead of createAndroidComposeRule<MainActivity> MainActivity.onCreate() already calls setContent, so the test's setContent { ChatScreen(...) } crashed with 'has already set content'. Switch to createComposeRule() which launches a bare ComponentActivity that doesn't call setContent, allowing the test to compose ChatScreen in isolation with a mock ChatViewModel. * fix(#302): use createAndroidComposeRule<ComponentActivity> + unmockkAll for instrumented tests ChatScreenTest was failing with 'No compose hierarchies found' because mockk mocks in instrumented tests weren't cleaned up between test runs. Switch from createComposeRule() to explicit createAndroidComposeRule<ComponentActivity>() and add @After tearDown() with unmockkAll() to ensure mock state doesn't leak between test methods. * fix(#302): wrap ChatScreen in HermesControlTheme in instrumented tests ChatScreen accesses MaterialTheme.colorScheme during composition (background gradient). Without a Material3 theme wrapper, the composable silently crashes and no compose hierarchy is registered — causing 'No compose hierarchies found' on any subsequent onNodeWithTag() call. Fix: wrap ChatScreen in HermesControlTheme inside setContent {}, matching how MainActivity renders the app. Also use createAndroidComposeRule<ComponentActivity> instead of createComposeRule() for explicit lifecycle management, and add unmockkAll() in @After to clean up mockk state between tests. * fix(ci): fix packaging resource paths and use pickFirsts for duplicate META-INF files Remove leading slashes from META-INF paths (AGP paths are relative, not absolute). Use pickFirsts instead of excludes to handle duplicate files from JUnit Jupiter JARs. * fix(#302): use createAndroidComposeRule<ComponentActivity> for test isolation createComposeRule() has a known race where the compose hierarchy is lost between sequential test methods on the emulator. createAndroidComposeRule<ComponentActivity> provides proper Activity lifecycle management while using the same lightweight empty activity that doesn't pre-set content (unlike MainActivity). * fix(#302): consolidate ChatScreenTest into single test method The Compose test rule's lifecycle gap between test methods on the emulator causes 'No compose hierarchies found' on consecutive setContent calls. Consolidating all three test scenarios into one invocation eliminates the cross-test teardown issue entirely. * fix(#302): mock ContextCompat to suppress permission dialog in test The ChatScreen LaunchedEffect requests POST_NOTIFICATIONS via ActivityResultContracts.RequestPermission(), which pauses the activity and disposes the compose hierarchy. Mocking ContextCompat.checkSelfPermission to return PERMISSION_GRANTED prevents this from happening during the test. * fix(#302): set connectionStatus=CONNECTED in ChatScreenTest The default ChatUiState has connectionStatus=DISCONNECTED, which makes isConnected=false, in turn disabling the chat input field (enabled = isConnected) and the send button (canSend checks isConnected). Set it to CONNECTED so both controls are active. * fix(#302): add ConnectionStatus import and fix ktlint wrapping --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Hermes Control
Remote control for your Hermes AI agent — from your pocket.
Features • Screenshots • Quick Start • Project Structure • Tech Stack • Contributing • Releases
Overview
Hermes Control is a native Android companion app for Hermes Agent — the open-source, multi-platform AI assistant by Nous Research.
Instead of pulling out a laptop or switching to Telegram/Discord, you can manage your Hermes agent directly from your phone:
- 💬 Chat with your agent in real time
- 🔧 Inspect and reconfigure system settings, profiles, and plugins
- 📊 Monitor gateway status, MCP servers, and webhook health
- 📋 Manage cron jobs, environment keys, and toolsets
- 🗂️ Track kanban boards and agent achievements
- 🔄 Pull-to-refresh every data screen
All this with a Material You design that adapts to your device's dynamic color theme.
Features
🤖 Agent Chat
- Real-time message exchange with your Hermes agent
- Session management with profile switching
- Message history with persistent storage (Room)
⚙️ System Management
- Profiles — View and switch between Hermes agent profiles
- Skills — Browse installed skills with inline descriptions
- Plugins — Manage active plugins and their configurations
- Toolsets — See which toolsets are enabled per profile
🛠️ Operations
- Cron Jobs — View scheduled tasks, their schedules, and last-run status
- Webhooks — Configure and test webhook endpoints
- Keys & Credentials — Securely manage environment variables
- Logs — Stream and filter agent logs
📡 Connectivity
- Gateway Status — Monitor WebSocket and API gateway health
- MCP Servers — Manage Model Context Protocol server connections
- Channels — View connected messaging platform status
📊 Productivity
- Kanban Boards — Track tasks across boards, columns, and cards
- System Info — Runtime environment and configuration overview
- Achievements — Agent milestone tracker
🎨 UX
- Material You — Dynamic color theming from your wallpaper
- Pull-to-Refresh — Swipe down on any data screen to reload
- Shared Scaffold — Consistent navigation and layout across all screens
- Scroll-aware TopBar — Collapsing top bar on scroll
Screenshots
📸 Screenshots coming soon. Build the app and see it in action!
| Chat | System | Profile | Kanban |
|---|---|---|---|
| 🔲 | 🔲 | 🔲 | 🔲 |
Quick Start
Prerequisites
| Tool | Version | Reason |
|---|---|---|
| JDK | 17+ | Kotlin/JVM compilation |
| Android Studio | Hedgehog+ | IDE + SDK management |
| Android SDK | 36 (compile), 26 (min) | Via Android Studio SDK Manager |
| Git | any | Clone the repo |
Clone & Build
git clone https://github.com/Hy4ri/hermes-mobile.git
cd hermes-mobile
./gradlew assembleDebug
The debug APK will be at app/build/outputs/apk/debug/app-debug.apk.
Build with custom version
./gradlew assembleRelease -PversionName="1.2.0" -PversionCode="120"
Note: Release builds require proper signing configuration. Ensure the environment variables
KEYSTORE_PATH,KEYSTORE_PASSWORD,KEY_ALIAS, andKEY_PASSWORDare set before building.
Install
adb install app/build/outputs/apk/debug/app-debug.apk
Or open the project in Android Studio and hit Run ▶.
Project Structure
hermes-mobile/
├── app/
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/m57/hermescontrol/
│ │ │ │ ├── Navigation.kt # Drawer + NavDisplay + entry wiring
│ │ │ │ ├── NavigationController.kt # Central navigation guard (dedup)
│ │ │ │ ├── NavigationKeys.kt # Screen key definitions
│ │ │ │ ├── MainActivity.kt # Single activity entry point
│ │ │ │ ├── data/
│ │ │ │ │ ├── model/ # API models / DTOs (20+ files)
│ │ │ │ │ ├── remote/ # Retrofit + OkHttp API client
│ │ │ │ │ ├── local/ # Room DB (Dao, Entity, Mapper)
│ │ │ │ │ └── ws/ # WebSocket TUI Gateway (JSON-RPC 2.0)
│ │ │ │ ├── notification/
│ │ │ │ │ └── ChatNotificationService.kt # Foreground service for reply alerts
│ │ │ │ ├── theme/
│ │ │ │ │ ├── Theme.kt # Material You dynamic theming
│ │ │ │ │ ├── Color.kt # Color palette definitions
│ │ │ │ │ ├── HermesStatusColors.kt # Semantic status colours (CompositionLocal)
│ │ │ │ │ ├── Type.kt # Typography scale
│ │ │ │ │ ├── Motion.kt # Shared transition animations
│ │ │ │ │ ├── Spacing.kt # Spacing tokens (CompositionLocal)
│ │ │ │ │ └── Shapes.kt # Shape tokens
│ │ │ │ ├── ui/
│ │ │ │ │ ├── common/ # Shared components
│ │ │ │ │ │ ├── HermesScaffold.kt # Standardised screen layout
│ │ │ │ │ │ ├── StateViews.kt # Loading, error, empty placeholders
│ │ │ │ │ │ └── SharedComponents.kt # SearchBar, FilterChips, ToggleRow, etc.
│ │ │ │ │ ├── chat/ # Chat screen + view model
│ │ │ │ │ ├── settings/ # Settings screen + view model
│ │ │ │ │ ├── connect/ # Connect / pairing screen
│ │ │ │ │ ├── profiles/ # Profile management
│ │ │ │ │ ├── skills/ # Skills browser
│ │ │ │ │ ├── plugins/ # Plugin management
│ │ │ │ │ ├── toolsets/ # Toolset viewer
│ │ │ │ │ ├── cron/ # Cron job viewer
│ │ │ │ │ ├── webhooks/ # Webhook management
│ │ │ │ │ ├── logs/ # Log stream viewer
│ │ │ │ │ ├── keys/ # Environment variable manager
│ │ │ │ │ ├── gateway/ # Gateway health monitor
│ │ │ │ │ ├── mcp/ # MCP server manager
│ │ │ │ │ ├── model/ # LLM model selector
│ │ │ │ │ ├── channels/ # Messaging channels
│ │ │ │ │ ├── pairing/ # Device pairing
│ │ │ │ │ ├── config/ # Raw config viewer
│ │ │ │ │ ├── system/ # System info screen
│ │ │ │ │ ├── kanban/ # Kanban board viewer
│ │ │ │ │ └── achievements/ # Agent achievements
│ │ │ │ └── util/ # Utility classes
│ │ │ │ └── CronExpressionFormatter.kt
│ │ │ ├── res/
│ │ │ │ ├── values/
│ │ │ │ │ └── strings.xml # String resources
│ │ │ │ ├── drawable/ # Vector icons & drawables
│ │ │ │ └── mipmap-*/ # Launcher icons
│ │ │ └── AndroidManifest.xml
│ │ └── test/ # Unit tests
│ │ └── kotlin/com/m57/hermescontrol/
│ └── build.gradle.kts # App-level build config
├── gradle/
│ └── libs.versions.toml # Version catalog
├── .github/
│ └── workflows/
│ ├── android.yml # CI (ktlint + unit tests)
│ └── release.yml # Release APK builder
├── build.gradle.kts # Root build config
├── settings.gradle.kts # Project settings
└── gradlew # Gradle wrapper
\n---
Tech Stack
| Category | Library | Purpose |
|---|---|---|
| Language | Kotlin 2.3+ | Primary language |
| UI | Jetpack Compose (BOM 2026.03.01) | Declarative UI |
| Design | Material3 (Compose) + Material You | Dynamic theming |
| Navigation | Navigation3 | Screen routing |
| Networking | Retrofit 2 + OkHttp 4 | REST API calls |
| Persistence | Room (SQLite) | Local message history |
| Encryption | AndroidX Security Crypto | Secure key storage |
| Background | Foreground Service | Notification polling (ChatNotificationService) |
| Serialization | Gson | JSON parsing |
| Testing | jUnit 5 + MockK + OkHttp MockWebServer | Unit testing |
| Linting | ktlint | Kotlin code style |
Architecture
The app follows a single-activity, Compose-first architecture with MVVM:
View (Composable) → ViewModel → ApiClient (Retrofit) → Hermes Agent (HTTP)
- Screens are pure Compose functions that observe
StateFlowfrom their ViewModel - ViewModels manage UI state and call the API client
- ApiClient is a Retrofit-based HTTP client that talks to the Hermes Agent REST API
- Navigation uses a custom NavHost with drawer-based routing
Contributing
PRs are welcome! Here's the workflow:
- Pick an issue — or create one first to discuss the change
- Branch from
main— usefeat/issue-N-descriptionorfix/issue-N-description - Implement — one PR per logical change
- CI must pass — ktlint + lintDebug + unit tests run on every PR
- Squash merge into
main
Code style
- ktlint enforces consistent formatting (
./gradlew ktlintCheck) - Use
HermesScaffoldfor new screens (never raw Material3Scaffold) - Compose parameters follow the standard order:
modifier, callbacks, content
PR checklist
- Branch from
mainand targetmain - CI is green (ktlint, lint, tests)
- No unused imports or dead code
contentDescriptionon allImage/Iconelements (accessibility)- Follows the existing pattern for similar screens
Releases
Pre-built APKs are available on the Releases page.
Each release is built automatically by CI when a maintainer pushes a v* tag:
git tag v1.2.0
git push origin v1.2.0
The release.yml workflow will:
- Run full CI (ktlint, lint, tests)
- Build a release APK with the correct version
- Create a GitHub Release with auto-generated notes
- Attach the APK as a downloadable asset
License
Copyright 2026 Hy4ri
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.