diff --git a/AGENTS.md b/AGENTS.md index 69cb07f5..0135e5a7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,14 +5,14 @@ This complements [README.md](README.md) (for humans) with agent-focused context. ## Project Overview -**Hermes Control** is a Jetpack Compose Android app — a mobile control panel for +**Hermes Mobile** is a Jetpack Compose Android app — a mobile control panel for [Hermes Agent](https://hermes-agent.nousresearch.com). It talks to the Hermes dashboard's REST API and WebSocket TUI Gateway (JSON-RPC 2.0) over a trusted LAN. - **Package:** `com.m57.hermescontrol` - **Min SDK 26 / Target SDK 36 / Compile SDK 36** -- **Kotlin 2.3.20**, KSP 2.3.9 (standalone versioning, NOT `kotlinVersion-kspVersion`) -- **Jetpack Compose**, Room 2.7.x, Navigation3, OkHttp WebSocket, Retrofit, Gson +- **Kotlin 2.4.10**, KSP 2.3.10 (standalone versioning, NOT `kotlinVersion-kspVersion`) +- **Jetpack Compose**, Room 2.7.x, Navigation3, OkHttp WebSocket, Retrofit, Kotlinx Serialization - **Auth:** `EncryptedSharedPreferences` (AES256-GCM), Bearer token (REST) + `?token=` (WS) ## Build & Test Commands @@ -28,6 +28,7 @@ If you have a local Android SDK (`ANDROID_HOME` set), these work: ``` **ktlint standalone** (no SDK needed): + ```bash # Download the matching binary curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.2.1/ktlint @@ -40,14 +41,17 @@ chmod +x ktlint ### CI pipeline (`.github/workflows/android.yml`) -| Job | Purpose | -|-----|---------| -| `ktlint` | ktlint 1.2.1 style check | -| `android-lint` | Android Lint | -| `unit-tests` | JUnit | -| `build` | assembleDebug (gated by the 3 above) | -| `release-compile` | compileReleaseKotlin — catches release-variant compilation issues (e.g. debugImplementation-scoped deps referenced in main source) | -| `ci-summary` | Aggregator (`if: always()`) — branch protection gates on THIS check | +| Job | Purpose | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `ktlint` | ktlint 1.2.1 style check + `checkColorLiterals` (hardcoded Color guard, issue #622) | +| `android-lint` | Android Lint | +| `unit-tests` | JUnit | +| `build` | assembleDebug (gated by the 3 above) | +| `release-compile` | compileReleaseKotlin — catches release-variant compilation issues (e.g. debugImplementation-scoped deps referenced in main source) | +| `instrumented-tests` | Compose UI tests on Android emulator (API 34 ATD) | +| `ci-summary` | Aggregator (`if: always()`) — branch protection gates on THIS check | + +Also: There is a separate workflow `merge-conflict-detector.yml` that auto-labels PRs with `has-conflicts`. Every Gradle job validates `gradle-wrapper.jar` and uses the remote build cache (`GRADLE_ENCRYPTED_KEY` secret). `concurrency.cancel-in-progress: true`. @@ -104,11 +108,13 @@ Some ViewModels (e.g. `AuthLoginViewModel`) are created at the Activity scope vi and cached state like `connectionSuccess` persists across screen entries. Always pair transient state flags with a `DisposableEffect` cleanup: + ```kotlin DisposableEffect(Unit) { onDispose { viewModel.clearTransientState() } } ``` + The cleanup function should reset self-transitioning flags (`connectionSuccess`, `isLoading`, `errorMessage`) so the screen can re-enter cleanly. @@ -117,9 +123,9 @@ The cleanup function should reset self-transitioning flags (`connectionSuccess`, `HermesWsClient` is a singleton that outlives individual screens. It must be explicitly disconnected on logout and reconnected after login: -| Event | Action | -|-------|--------| -| Logout | `HermesWsClient.disconnect()` before clearing tokens | +| Event | Action | +| ------------- | ------------------------------------------------------ | +| Logout | `HermesWsClient.disconnect()` before clearing tokens | | Login success | `HermesWsClient.connect()` after `ApiClient.rebuild()` | The singleton's `connect()` has a guard (`if connected → skip`) so it's safe to @@ -193,10 +199,18 @@ breakdown, edge cases, and timeline of previous occurrences. ### Theme -`Theme.kt` has `ENABLE_DYNAMIC_COLOR = true` (Material You on API 31+). Semantic -status colors (success/warning/error/info) are ALWAYS brand-defined via -`LocalHermesStatusColors`, never dynamic. Access them via -`LocalHermesStatusColors.current.success`, not `MaterialTheme.colorScheme`. +`Theme.kt` uses a preset-based theme system with 6 built-in presets: Default, +Monochrome, Gruvbox, Catppuccin, AMOLED, and Neon Noir. Each preset provides both +light and dark color schemes plus matching semantic status colors. + +Dynamic color (Material You on API 31+) is controlled by a `useDynamicColors` +parameter on `HermesControlTheme`, not a compile-time constant. When dynamic +colors are active they override the preset's `ColorScheme`, but semantic status +colors (success/warning/error/info) are ALWAYS resolved from the active preset +via `LocalHermesStatusColors` — never from `MaterialTheme.colorScheme`. + +Access status colors via `LocalHermesStatusColors.current.success`, not +`MaterialTheme.colorScheme`. ## Git Workflow @@ -216,6 +230,7 @@ gh pr create --title "fix(#N): description" --body "Closes #N" ### Commit Conventions — STRICT **⚠ Agent-authored commits:** Do NOT override git authorship (`--author`) with the agent's identity. Use the default git config. No `Co-Authored-By` or `Generated with` trailers. + - **Short commits:** subject line + max 2 lines of body. Split into atomic commits rather than writing long bodies. - **Conventional Commits** types: `feat`, `fix`, `refactor`, `docs`, `test`, `ci`, `chore`. - Bug fixes annotated inline with the tracking issue/regression ID. @@ -244,7 +259,7 @@ gh pr create --title "fix(#N): description" --body "Closes #N" (`remember {}`, `buildAnnotatedString`, `LaunchedEffect`). Extract to a local `val` at the composable scope first. - Don't add new screens without checking if an existing one already covers the - functionality (19+ screens exist). Extend rather than duplicate. + functionality (28+ screens exist). Extend rather than duplicate. - **⚠ Never scope a dependency to `debugImplementation` if its import is used in `main/` source code.** The CI `release-compile` job catches this, but save the cycle. `okhttp3.logging.HttpLoggingInterceptor` is the classic example — it's @@ -252,22 +267,48 @@ gh pr create --title "fix(#N): description" --body "Closes #N" `debugImplementation`. If you want it debug-only, wrap the usage in `if (BuildConfig.DEBUG)` or extract it behind an interface. +### Hardcoded Color Guard (issue #622) + +A custom Gradle task `checkColorLiterals` fails the build if `Color(0x...)` hex +literals or named `Color.White`/`Color.Black`/etc. appear outside the allowed +paths (`theme/`, `*Preview.kt`, `PairingScreen.kt`, `AuthLoginScreen.kt`). + +Use `MaterialTheme.colorScheme.`, `LocalHermesStatusColors.current.`, +or `Color.Transparent` / `Color.Unspecified` instead. The guard runs in CI as part +of the `ktlint` job. + ## Project Layout ``` com.m57.hermescontrol/ -├── data/ local (AuthManager), remote (Retrofit), ws (WebSocket), model -├── notification/ ChatNotificationService (foreground service) -├── theme/ Color, Theme, Motion, Spacing, Shapes, Type -├── ui/ chat, settings, common (HermesScaffold, StateViews), + 17 screens -├── util/ CronExpressionFormatter, etc. -├── Navigation.kt Drawer + NavDisplay + entry wiring -├── NavigationController.kt Central navigation guard (dedup) +├── data/ +│ ├── config/ ServerStore, ConnectionProfile, migrations +│ ├── local/ AuthManager, Room (ChatMessageEntity/Dao, HermesDatabase), AnalyticsCacheStore +│ ├── model/ 40+ data classes for API responses + requests +│ ├── remote/ ApiClient, Retrofit service, OkHttp provider, cookie management +│ ├── session/ ActiveSessionHolder +│ └── ws/ HermesWsClient, JSON-RPC models, WsEvent, BillingRepository +├── notification/ ChatNotificationService, NotificationReplyReceiver +├── theme/ Color, Theme, Motion, Spacing, Shapes, Type, HermesStatusColors +│ └── presets/ Default, Monochrome, Gruvbox, Catppuccin, AMOLED, NeonNoir +├── ui/ +│ ├── common/ HermesScaffold, StateViews, SharedComponents, DetailDialog, DetailRows +│ └── 28 feature packages (achievements, analytics, authlogin, billing, channels, +│ chat, config, connect, cron, gateway, kanban, keys, landing, logs, mcp, +│ model, pairing, plugins, process, profiles, providers, sessions, settings, +│ skills, system, toolsets, webhooks) +├── util/ CronExpressionFormatter, LocaleContextWrapper +├── HermesControlApp.kt Application class +├── Navigation.kt Drawer + NavDisplay + entry wiring +├── NavigationController.kt Central navigation guard (dedup) +├── NavigationKeys.kt @Serializable NavKey data objects (28 screens + 6 settings sub-pages) +├── ScreenRegistry.kt entry registrations for all NavKeys └── MainActivity.kt ``` ## Further Reading - [README.md](README.md) — human-facing overview, features, screenshots, tech stack -- [TEST_INFRA.md](TEST_INFRA.md) / [TEST_READY.md](TEST_READY.md) — test infrastructure notes +- [CONTRIBUTING.md](CONTRIBUTING.md) — contributor workflow, PR checklist, code style - [.github/workflows/android.yml](.github/workflows/android.yml) — CI pipeline source of truth +- [.github/workflows/merge-conflict-detector.yml](.github/workflows/merge-conflict-detector.yml) — auto-labels conflicting PRs diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2a2bffea..7724270a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -68,6 +68,13 @@ We enforce Kotlin coding conventions and Jetpack Compose best practices. ### Compose Guidelines - Standard screen structures must use `HermesScaffold` rather than raw Material3 `Scaffold`. - Composable parameters must follow the standard order: `modifier` first, then event callbacks, and finally children content. +- Do **not** apply `paddingValues` on inner content inside `HermesScaffold` — the scaffold already handles top bar padding. See `AGENTS.md` for the full breakdown of this recurring bug. +- Every data screen must implement `LoadingState`, `ErrorState`, and `EmptyState` branches in its `when { }` block. + +### Color Usage +- **No hardcoded `Color(0x...)` literals** outside `theme/`, `*Preview.kt`, and auth screens. A Gradle task (`checkColorLiterals`) enforces this in CI. +- Use `MaterialTheme.colorScheme.` or `LocalHermesStatusColors.current.` instead. +- `Color.Transparent` and `Color.Unspecified` are allowed. --- @@ -77,7 +84,9 @@ Before submitting your PR, please verify: - [ ] Your branch is up-to-date with `main`. - [ ] Local build and `ktlint` checks pass successfully. +- [ ] `checkColorLiterals` passes (no hardcoded Color literals outside theme/). - [ ] No unused imports, unused parameters, or dead code. - [ ] Every `Image` and `Icon` element has a descriptive `contentDescription` for accessibility. -- [ ] New components match the UI/UX style of similar existing screens. +- [ ] New screens use `HermesScaffold` and implement Loading/Error/Empty states. +- [ ] New components match the UI/UX style of similar existing screens (28+ screens for reference). - [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) and are atomic (subject + ≤2 lines body). diff --git a/README.md b/README.md index eb47cde0..0eb008b2 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,14 @@ ## Features -- **Real-Time Chat:** Message your agent with Room-backed local database history. -- **System Config:** Manage active profiles, installed skills, plugins, and LLM model selections. -- **Operations:** Stream and filter live logs, manage cron jobs, edit environment keys, and test webhooks. -- **Gateway Status:** Monitor WebSocket connection, MCP servers, and messaging channel status. -- **Productivity:** View and manage tasks via integrated Kanban boards and track agent milestones. -- **Modern UX:** Native Material You design supporting dynamic color theming, scroll-aware TopBar, and pull-to-refresh. +- **Real-Time Chat:** Message your agent with Room-backed local database history and inline reply notifications. +- **System Config:** Manage active profiles, installed skills, plugins, toolsets, and LLM model/provider selections. +- **Operations:** Stream and filter live logs, manage cron jobs, edit environment keys, test webhooks, and monitor processes. +- **Gateway Status:** Monitor WebSocket connection, MCP servers, messaging channels, and OAuth providers. +- **Productivity:** View and manage tasks via integrated Kanban boards, track agent milestones, and browse session history. +- **Analytics & Billing:** Usage analytics dashboard and billing/subscription management. +- **Theming:** 6 built-in color presets (Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Neon Noir) plus Material You dynamic colors on supported devices. +- **Modern UX:** Native Material 3 design with pull-to-refresh, scroll-aware TopBar, and customizable bottom navigation. --- @@ -108,23 +110,28 @@ The **Pairing** screen lets you approve or revoke agents and services that are t ``` app/src/main/java/com/m57/hermescontrol/ -├── data/ # Local (Room, EncryptedSharedPreferences) & Remote (Retrofit, OkHttp WS) -├── notification/ # Foreground service for message notifications -├── theme/ # Material You design system, status colors, spacing, and typography -└── ui/ # Compose screens (Chat, Settings, Profiles, Kanban, etc.) + Navigation +├── data/ # Local (Room, AuthManager), Remote (Retrofit, OkHttp), WS (WebSocket), Models +├── notification/ # Foreground service + inline reply for chat notifications +├── theme/ # Preset-based design system (6 themes), status colors, spacing, typography +├── ui/ # 28 Compose feature screens + common components (HermesScaffold, StateViews) +├── util/ # CronExpressionFormatter, LocaleContextWrapper +└── Navigation*.kt # Navigation3 wiring, keys, screen registry, controller ``` --- ## Tech Stack -- **Language:** Kotlin 2.4.0 with KSP compiler plugin +- **Language:** Kotlin 2.4.10 with KSP 2.3.10 compiler plugin - **UI & Layout:** Jetpack Compose (BOM 2026.03.01) & Material 3 / Material You - **Navigation:** Navigation3 (Compose-first Routing) -- **Networking:** Retrofit 3.0.0, OkHttp 5.4.0, Gson 2.14.0 -- **Database:** Room 2.7.1 with SQLCipher encryption -- **Security:** `EncryptedSharedPreferences` (AES256-GCM) -- **Formatting:** `ktlint` style rules (checked automatically in CI) +- **Networking:** Retrofit 3.0.0, OkHttp 5.4.0, Kotlinx Serialization 1.11.0 +- **Database:** Room 2.7.1 with SQLCipher 4.17.0 encryption +- **Security:** `EncryptedSharedPreferences` (AES256-GCM), DataStore +- **Theming:** 6 built-in presets (Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Neon Noir) + Material You dynamic colors +- **Image Loading:** Coil 2.7.0 +- **Testing:** JUnit 5, MockK, Turbine, Espresso, Compose UI testing +- **Formatting:** `ktlint` 1.2.1 style rules (checked automatically in CI) ---