diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a7d858ca..b2b73c01 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,7 +19,7 @@ Fixes # ## How to test - + 1. 2. @@ -32,4 +32,5 @@ Fixes # - [ ] `./gradlew testDebugUnitTest` green (or CI unit-tests job) - [ ] `checkColorLiterals` passes (no hardcoded Color outside theme/) - [ ] Navigation goes through `NavigationController.navigateTo()` (not `backStack.add`) -- [ ] Tested on device/emulator or verified via CI APK +- [ ] For UI changes, exercised the changed behavior on a device/emulator and recorded the result above (or explicitly documented the unverified gate) +- [ ] Documentation matches changed behavior; UI changes follow DESIGN.md diff --git a/AGENTS.md b/AGENTS.md index 254d6894..d87443ad 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,11 +2,12 @@ Guidance for AI coding agents working on this repository. This complements [README.md](README.md) (for humans) and [DESIGN.md](DESIGN.md) -(architecture & design rationale) with agent-focused context. Before making any -non-trivial architectural change — new screens, navigation changes, theme/state -patterns, module boundaries — read `DESIGN.md` first and keep changes consistent -with the decisions recorded there. If a change would contradict `DESIGN.md`, -flag it explicitly rather than silently diverging. +(visual and interaction requirements) with agent-focused operational and +architecture context. Before changing navigation, state ownership, or module +boundaries, read the relevant implementation and the conventions below. For UI +changes, also read `DESIGN.md`; for theme implementation, read +[`THEMES.md`](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md). +Flag conflicts between requirements and implementation instead of silently diverging. ## Project Overview @@ -16,9 +17,9 @@ 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 37 / Compile SDK 37** -- **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) +- **Kotlin and KSP:** versions are pinned in [`gradle/libs.versions.toml`](gradle/libs.versions.toml). KSP uses standalone versioning, NOT `kotlinVersion-kspVersion`. +- **Jetpack Compose**, Room, Navigation3, OkHttp WebSocket, Retrofit, Kotlinx Serialization. See the version catalog and [`app/build.gradle.kts`](app/build.gradle.kts) for versions and dependency scopes. +- **Auth:** `EncryptedSharedPreferences` (AES256-GCM); connection/authentication flows are documented in [README.md](README.md#authentication). Trace the active REST and WebSocket auth paths before changing them. ## Build & Test Commands @@ -32,7 +33,7 @@ If you have a local Android SDK (`ANDROID_HOME` set), these work: ./gradlew ktlintCheck # style check ``` -**ktlint standalone** (no SDK needed): +**ktlint standalone fallback** (no SDK needed; not the authoritative CI gate): ```bash # Download the matching binary @@ -68,7 +69,7 @@ Requires `permissions: contents: write` on the `build-release` job. ## Code Style -- **ktlint 1.8.0** is enforced in CI. Run `./ktlint --format` before pushing. +- **ktlint 1.8.0** is enforced in CI. With an Android SDK, run `./gradlew ktlintFormat` followed by `./gradlew ktlintCheck`. Standalone formatting is only a fallback; it does not replace the Gradle CI check. - Import ordering is the #1 CI failure: ktlint enforces ASCII-lexicographic order (uppercase before lowercase: `LaunchedEffect` before `collectAsState`). - `const val` must use SCREAMING_SNAKE_CASE. @@ -147,12 +148,9 @@ call unconditionally. ### ⚠ HermesScaffold Padding Foot-Gun -**This is the #1 recurring bug in this codebase.** It has been re-introduced on 4+ screens -across 3+ PRs (Settings, Achievements, Webhooks, Config — PRs #445, #454, #455). - -**Root cause:** `HermesScaffold` wraps content in an internal `Box(Modifier.padding(paddingValues))` -that already offsets for the top bar. But it also passes `paddingValues` into the content lambda, -which looks like it should be applied — and every new screen does exactly that: +`HermesScaffold` handles system insets and the dynamic top-bar offset in its +internal content wrapper. Although it passes `paddingValues` to the content +lambda, applying those values again double-stacks the padding: ```kotlin HermesScaffold(...) { paddingValues -> // ← scaffold already handles top bar offset via Box @@ -165,11 +163,6 @@ HermesScaffold(...) { paddingValues -> // ← scaffold already handles top } ``` -**The deeper issue:** Passing `paddingValues` into the lambda implies "you need to use this," -when the scaffold has ALREADY pre-applied it in its own outer `Box`. This API design creates -a natural foot-gun: every developer instinctively adds `.padding(paddingValues)` on inner -content because it seems correct. - **Correct pattern — do NOT apply `paddingValues` on inner content:** ```kotlin @@ -185,21 +178,21 @@ Column( modifier = Modifier.fillMaxSize(), // no .padding(paddingValues)!! ) { ... } -// ✅ Loading/Error/Empty states — these DO need it: -LoadingState(modifier = Modifier.padding(paddingValues)) +// ✅ Loading/Error/Empty states inside HermesScaffold content follow the same rule: +LoadingState(modifier = Modifier.fillMaxSize()) ``` -**Quick test:** If your screen's top gap is wider than CronJobsScreen's, you've double-stacked. - -**See also:** `references/hermes-scaffold-padding.md` in the skill doc for the full -breakdown, edge cases, and timeline of previous occurrences. +This applies to every branch inside the content lambda. A state view outside +`HermesScaffold` must follow its own parent layout's inset contract instead. +See [`HermesScaffold.kt`](app/src/main/java/com/m57/hermescontrol/ui/common/HermesScaffold.kt) +for the current inset implementation. ### Room Persistence - `ChatMessageEntity` / `ChatMessageDao` / `HermesDatabase` — chat messages survive app kills. `getMessagesForSession()` returns `suspend fun ...: List` (not `Flow` — the caller controls the coroutine scope). -- Room 2.7.x requires `room { schemaDirectory("$projectDir/schemas") }` DSL. +- Room schema configuration is defined in `app/build.gradle.kts`; preserve schema export and migrations when updating Room. - `ChatViewModel` extends `AndroidViewModel` (needs Application for DB access). ### Theme @@ -230,7 +223,7 @@ Access status colors via `LocalHermesStatusColors.current.success`, not ```bash git checkout dev && git pull origin dev git checkout -b fix/issue-N-description # or feat/... -# make changes, run ./ktlint --format +# make changes, run ./gradlew ktlintFormat ktlintCheck (requires Android SDK) git commit -m "fix(#N): description" git push -u origin HEAD gh pr create --base dev --title "fix(#N): description" --body "Closes #N" @@ -316,8 +309,8 @@ com.m57.hermescontrol/ ## Further Reading - [README.md](README.md) — human-facing overview, features, screenshots, tech stack -- [DESIGN.md](DESIGN.md) — architecture and design decisions, rationale for the - patterns in this doc (Navigation3, HermesScaffold, theme presets, etc.) +- [DESIGN.md](DESIGN.md) — visual and interaction requirements, accessibility, and token source map +- [THEMES.md](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md) — theme template, dispatcher, and mode fallback implementation - [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 9bf2da36..d0283d0e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ If an open PR already covers it, review or improve that one instead of opening a ## PR Workflow -All code changes must go through a pull request (PR) targeting the `dev` staging branch. Directly pushing to `main` or `dev` is not allowed **except for trivial changes the maintainer explicitly okays** — when in doubt, open a PR. +Every change must go through a pull request (PR) targeting the `dev` staging branch. Do not push directly to `main` or `dev`. 1. **Pick or open an issue** to discuss the changes you want to make. 2. **Create a branch** off `dev` using the following naming convention: @@ -30,7 +30,7 @@ All code changes must go through a pull request (PR) targeting the `dev` staging 3. **Implement your changes** and format them locally (see Code Style). 4. **Submit a PR** targeting the `dev` branch, filling the PR template. 5. **Ensure all CI checks pass** (ktlint, Android Lint, unit tests, build). -6. **Rebase onto `dev` before merge.** Maintainers squash-merge; a stale branch's version of an unrelated file can silently overwrite recent fixes on `dev` when squashed. `git fetch origin dev && git rebase origin/dev` first. +6. **Rebase onto `dev` before merge.** Run `git fetch origin dev && git rebase origin/dev`, resolve conflicts carefully, and re-run verification on the rebased result. Maintainers squash-merge; green CI does not by itself authorize a merge. --- @@ -51,9 +51,8 @@ Keep commits **atomic**: one subject line (≤72 chars) + max 2 lines of body. I ``` fix(#431): resume last session from Room cache on cold start -Reorders init to show cached messages before WS connects, -and resumes the last session on GatewayReady instead of -always creating a blank new one. +Shows cached messages before WS connects and resumes the last +session on GatewayReady instead of creating a blank one. ``` --- @@ -63,7 +62,7 @@ always creating a blank new one. If you use AI coding tools (including agents) to contribute: - **Never** add the AI tool as author, co-author, or `Co-Authored-By` in commit metadata. -- Direct AI agents to [`AGENTS.md`](AGENTS.md) and [`DESIGN.md`](DESIGN.md) in the repo root — `AGENTS.md` contains operational conventions, build quirks, and testing rules, while `DESIGN.md` defines the normative design tokens, layout rules, and component styling contracts. +- Direct AI agents to [`AGENTS.md`](AGENTS.md) and [`DESIGN.md`](DESIGN.md) in the repo root — `AGENTS.md` contains operational and architecture conventions, while `DESIGN.md` defines visual, interaction, and accessibility requirements and links to the token sources. Theme implementation is documented in [`THEMES.md`](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md). --- @@ -94,7 +93,7 @@ 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. +- Follow existing Compose APIs: required parameters first, `modifier` as the first optional parameter, then other optional parameters; keep trailing composable content last. - 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. @@ -108,7 +107,8 @@ We enforce Kotlin coding conventions and Jetpack Compose best practices. - Unit tests: `./gradlew testDebugUnitTest` (MockK). Instrumented Compose UI tests run in CI on an emulator. - **Tests must not touch real on-device storage.** Use `@get:Rule TemporaryFolder` (or Room `inMemoryDatabaseBuilder`) — never write to real app data or external storage. CI/emulator environments are ephemeral; leaking state breaks the next run. -- Mock time/dispatchers explicitly (`StandardTestDispatcher` + `Dispatchers.setMain`) — see `AGENTS.md` test references. +- Mock time/dispatchers explicitly (`StandardTestDispatcher` + `Dispatchers.setMain`), restore `Dispatchers.Main` in teardown, and cancel test-owned scopes. Follow neighboring tests for the affected component. +- For UI changes, exercise the changed flow on a device or emulator and record the behavior observed. A built or downloaded CI APK alone is not behavioral verification. If device verification is unavailable, state that gate explicitly rather than marking it passed. --- @@ -129,8 +129,9 @@ Before submitting your PR, please verify: - [ ] `checkColorLiterals` passes (no hardcoded Color literals outside theme/). - [ ] `./gradlew testDebugUnitTest` passes locally (or CI unit-tests job is green). - [ ] No unused imports, unused parameters, or dead code. -- [ ] Every `Image` and `Icon` element has a descriptive `contentDescription` for accessibility. +- [ ] Meaningful images and icon-only actions have localized accessible labels; decorative images/icons use `contentDescription = null` to avoid redundant announcements. - [ ] New screens use `HermesScaffold` and implement Loading/Error/Empty states. -- [ ] UI changes follow [`DESIGN.md`](DESIGN.md) (no emoji status glyphs, no FABs, verified >=3:1 contrast). -- [ ] New components match the UI/UX style of similar existing screens (28+ screens for reference). +- [ ] UI changes follow [`DESIGN.md`](DESIGN.md), including applicable contrast, touch-target, font-scaling, and RTL requirements. +- [ ] Changed UI behavior was exercised on a device/emulator; the PR records the result or explicitly names the unverified gate. +- [ ] New components match the UI/UX style of similar existing screens. - [ ] Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) and are atomic (subject + ≤2 lines body). diff --git a/DESIGN.md b/DESIGN.md index d089628b..86672bcc 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,236 +1,63 @@ ---- -version: "alpha" -name: "Hermes Mobile Design System" -description: "High-density, minimal Material 3 interface for the Hermes Agent Android client" -omitted: [] -colors: - # Slate Dark Baseline (DefaultScheme.kt) - bg-root: "#0F1416" - bg-surface: "#0F1416" - bg-surface-container-lowest: "#0F1416" - bg-surface-container-low: "#171C1E" - bg-surface-container: "#1B2022" - bg-surface-container-high: "#252B2D" - bg-surface-container-highest: "#303638" - bg-surface-variant: "#171C1E" +# Hermes Mobile Design System - # Foreground / Text Tokens - text-primary: "#DEE3E5" - text-secondary: "#CEE7EC" - text-muted: "#BEC8CA" - text-on-primary: "#0F1416" - text-on-secondary: "#0F1416" - text-on-tertiary: "#0F1416" - text-outline: "#889294" +## Scope and sources of truth - # Accents & Containers (Slate Bright Dark Mode) - primary: "#80D5D2" - primary-container: "#303638" - on-primary-container: "#DEE3E5" - secondary: "#B2CBD0" - secondary-container: "#252B2D" - on-secondary-container: "#CEE7EC" - tertiary: "#A3C9EC" - tertiary-container: "#303638" - on-tertiary-container: "#DEE3E5" +This document defines visual and interaction requirements for new or changed UI. +It is not an architecture manual or a claim that every existing screen already +meets every requirement. Flag existing deviations when touching the relevant UI; +do not silently redesign unrelated screens. - # Semantic Status Tokens (HermesStatusColors.kt & DefaultScheme.kt) - status-success: "#81D692" - status-success-container: "#252B2D" - status-on-success: "#0F1416" - status-warning: "#ECC248" - status-warning-container: "#252B2D" - status-on-warning: "#0F1416" - status-error: "#FFB4AB" - status-error-container: "#252B2D" - status-on-error: "#0F1416" - status-on-error-container: "#FFB4AB" - status-info: "#8ECEFF" - status-info-container: "#252B2D" - status-on-info: "#0F1416" +- [README.md](README.md): installation, connection, and product overview. +- [CONTRIBUTING.md](CONTRIBUTING.md): contribution workflow and verification. +- [AGENTS.md](AGENTS.md): operational rules and architecture conventions. +- [THEMES.md](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md): theme implementation and extension guide. - # Code Block & Terminal Tokens (Color.kt) - terminal-bg: "#1E1E1E" - terminal-border: "#333333" - terminal-text: "#D4D4D4" - terminal-muted: "#808080" - code-keyword: "#569CD6" - code-string: "#CE9178" - code-comment: "#6A9955" - code-number: "#B5CEA8" +Kotlin sources are authoritative for implemented token values and component APIs. +Use this document for intent; investigate and report conflicts rather than assuming +that either stale prose or an existing deviation grants permission to change behavior. - # Diff Viewer Tokens - diff-add-bg: "rgba(61, 220, 132, 0.15)" - diff-add-text: "#3DDC84" - diff-delete-bg: "rgba(255, 92, 92, 0.15)" - diff-delete-text: "#FF5C5C" +## Token source map -typography: - font-sans: "Roboto, 'Segoe UI', system-ui, sans-serif" - font-mono: "'JetBrains Mono', 'Roboto Mono', monospace" +Use semantic Kotlin tokens, not copied hex values or a parallel web/CSS token set. +Android layout dimensions use `dp`; text sizes, line heights, and tracking use `sp`. - display-large: - fontFamily: "{typography.font-sans}" - fontSize: "44px" - fontWeight: 700 - lineHeight: 52px - letterSpacing: "-0.5px" - display-medium: - fontFamily: "{typography.font-sans}" - fontSize: "36px" - fontWeight: 700 - lineHeight: 44px - letterSpacing: "-0.25px" - display-small: - fontFamily: "{typography.font-sans}" - fontSize: "30px" - fontWeight: 600 - lineHeight: 38px - letterSpacing: "0px" - headline-large: - fontFamily: "{typography.font-sans}" - fontSize: "28px" - fontWeight: 600 - lineHeight: 34px - letterSpacing: "0px" - headline-medium: - fontFamily: "{typography.font-sans}" - fontSize: "24px" - fontWeight: 600 - lineHeight: 30px - letterSpacing: "0px" - headline-small: - fontFamily: "{typography.font-sans}" - fontSize: "20px" - fontWeight: 600 - lineHeight: 26px - letterSpacing: "0.1px" - title-large: - fontFamily: "{typography.font-sans}" - fontSize: "20px" - fontWeight: 600 - lineHeight: 26px - letterSpacing: "0.1px" - title-medium: - fontFamily: "{typography.font-sans}" - fontSize: "16px" - fontWeight: 500 - lineHeight: 22px - letterSpacing: "0.15px" - title-small: - fontFamily: "{typography.font-sans}" - fontSize: "14px" - fontWeight: 500 - lineHeight: 20px - letterSpacing: "0.1px" - body-large: - fontFamily: "{typography.font-sans}" - fontSize: "16px" - fontWeight: 400 - lineHeight: 24px - letterSpacing: "0.25px" - body-medium: - fontFamily: "{typography.font-sans}" - fontSize: "14px" - fontWeight: 400 - lineHeight: 20px - letterSpacing: "0.25px" - body-small: - fontFamily: "{typography.font-sans}" - fontSize: "12px" - fontWeight: 400 - lineHeight: 16px - letterSpacing: "0.4px" - label-large: - fontFamily: "{typography.font-sans}" - fontSize: "14px" - fontWeight: 500 - lineHeight: 20px - letterSpacing: "0.1px" - label-medium: - fontFamily: "{typography.font-sans}" - fontSize: "12px" - fontWeight: 500 - lineHeight: 16px - letterSpacing: "0.5px" - label-small: - fontFamily: "{typography.font-sans}" - fontSize: "11px" - fontWeight: 500 - lineHeight: 16px - letterSpacing: "0.5px" - -rounded: - none: "0dp" - extra-small: "4dp" - small: "8dp" - medium: "12dp" - large: "16dp" - extra-large: "28dp" - full: "9999dp" - -spacing: - xs: "2dp" - sm: "6dp" - md: "12dp" - lg: "18dp" - xl: "24dp" - xxl: "36dp" - -components: - top-app-bar: - backgroundColor: "{colors.bg-surface}" - textColor: "{colors.text-primary}" - height: "56dp" - chat-user-bubble: - backgroundColor: "{colors.primary}" - textColor: "{colors.text-on-primary}" - rounded: "{rounded.medium}" - padding: "10dp 14dp" - chat-assistant-bubble: - backgroundColor: "{colors.bg-surface-container}" - textColor: "{colors.text-primary}" - rounded: "{rounded.medium}" - padding: "10dp 14dp" - tool-chip: - backgroundColor: "{colors.bg-surface-container-high}" - textColor: "{colors.text-primary}" - rounded: "{rounded.full}" - padding: "4dp 10dp" - code-card: - backgroundColor: "{colors.terminal-bg}" - textColor: "{colors.terminal-text}" - rounded: "{rounded.small}" - padding: "8dp 12dp" ---- +| Concern | Authoritative source | +| --- | --- | +| Presets and mode selection | [Theme.kt](app/src/main/java/com/m57/hermescontrol/theme/Theme.kt) | +| Material slot mapping | [PaletteTemplate.kt](app/src/main/java/com/m57/hermescontrol/theme/PaletteTemplate.kt) | +| Palette values | [presets/](app/src/main/java/com/m57/hermescontrol/theme/presets/) | +| Semantic status model | [HermesStatusColors.kt](app/src/main/java/com/m57/hermescontrol/theme/HermesStatusColors.kt) | +| Terminal, code, and diff colors | [Color.kt](app/src/main/java/com/m57/hermescontrol/theme/Color.kt) | +| Typography | [Type.kt](app/src/main/java/com/m57/hermescontrol/theme/Type.kt) | +| Spacing and shapes | [Spacing.kt](app/src/main/java/com/m57/hermescontrol/theme/Spacing.kt), [Shapes.kt](app/src/main/java/com/m57/hermescontrol/theme/Shapes.kt) | +| Motion | [Motion.kt](app/src/main/java/com/m57/hermescontrol/theme/Motion.kt) | ## Overview -Hermes Mobile (`com.m57.hermescontrol`) is a high-density, keyboard-first, distraction-free Android client for the Hermes Agent platform. The UI philosophy prioritizes information density, low battery drain (OLED-friendly), and instantaneous feedback over decorative visual noise. +Hermes Mobile (`com.m57.hermescontrol`) is a native Android client for the Hermes Agent platform. Prioritize useful information density, clear touch interactions, and timely, honest feedback over decorative visual noise. Density must not compromise accessibility. The visual language follows Material 3 guidelines strictly adapted for developer utilities: - **Tone**: Technical, focused, utilitarian, crisp. -- **Form Factors**: Native Android phones, foldables, and tablets via adaptive Navigation 3 layouts. +- **Form Factors**: Verify changed layouts at narrow phone widths and larger window sizes; Navigation 3 alone does not establish adaptive-layout support. - **Core Principle**: Zero fluff. Fast render cycles, explicit states, flat surfaces, and zero visual ambiguity. ## Colors -The app supports 6 dynamic presets managed by `PaletteTemplate.kt` (`Default/Slate`, `Monochrome`, `Gruvbox`, `Catppuccin`, `AMOLED`, `Nord`), plus dynamic Android 12+ wallpaper theming: +The app supports 6 built-in presets (`Default/Slate`, `Monochrome`, `Gruvbox`, `Catppuccin`, `AMOLED`, `Nord`), plus optional Android 12+ wallpaper theming. `Theme.kt` selects the preset; `PaletteTemplate.kt` maps its colors to Material slots. AMOLED is dark-only; light mode falls back to Default. 1. **Surfaces**: - - `bg-root`: Deep canvas background (`#0F1416` in Default Dark, `#000000` in AMOLED). - - `bg-surface`: Top app bars, bottom sheets, navigation drawer background. - - `bg-surface-container`: Assistant message bubbles, session list cards, settings group panels (`#1B2022`). - - `bg-surface-container-high`: Dialogs, interactive chips, elevated tool surfaces (`#252B2D`). - - `bg-surface-container-highest`: Selected containers and active pills (`#303638`). + - `MaterialTheme.colorScheme.background`: Main canvas and full-bleed chat background. + - `surface` and `surfaceContainer*`: Use the existing component's semantic surface tier for bars, panels, cards, and sheets. + - Pair surfaces with their corresponding foreground tokens; do not assume Default Dark values apply to light or dynamic themes. 2. **Accents & Tints**: - - Primary accent (`#80D5D2` Slate Bright Teal) is reserved for interactive controls, user bubbles, selected drawer icons, and active tabs. + - Primary accent is reserved for interactive controls, user bubbles, selected drawer icons, and active tabs. - Selected navigation drawer items MUST tint icons directly to `primary` to prevent low-contrast grey-on-grey visual loss. 3. **Semantic Status Colors (`HermesStatusColors`)**: - - Status indicators (`success`, `warning`, `error`, `info`) must always satisfy a minimum **3:1 contrast ratio** against their respective container backgrounds (`onErrorContainer` on `errorContainer`). - - Every theme preset must explicitly define `onErrorContainer` (`#FFB4AB` in Default Dark) to prevent invisible text bugs. + - Access semantic status colors through `LocalHermesStatusColors.current`, including when dynamic Material colors are active. Do not substitute Material accent colors for success/warning/error/info. + - Pair status colors with icons or labels; color alone is insufficient, especially in Monochrome and AMOLED. + - Every preset must explicitly define `onErrorContainer`. Apply the accessibility contrast requirements below to the actual foreground/background pair. ## Typography -- **UI Fonts**: System `Roboto` / sans-serif with strict semantic scaling to prevent layout shifts. -- **Code & Logs**: Tabular `JetBrains Mono` for terminal traces, tool payloads, JSON viewers, and memory tokens. +- **UI Fonts**: `FontFamily.Default`, using Material typography roles. Do not assume a particular system font on every device. +- **Code & Logs**: `FontFamily.Monospace`; do not promise JetBrains Mono or another named face without a bundled font and explicit wiring. - **Scale Discipline (`Type.kt`)**: - Screen titles: `20sp` (`title-large`) with 0.1sp tracking. - Card headings: `16sp` (`title-medium`) with 0.15sp tracking. @@ -240,9 +67,9 @@ The app supports 6 dynamic presets managed by `PaletteTemplate.kt` (`Default/Sla ## Layout - **Scaffolding (`HermesScaffold`)**: - - Global `TopAppBar` containing screen title, connection indicator, profile switch dropdown, and quick action icon buttons. + - Use the shared scaffold's top bar and supported action slots; do not duplicate its insets or hardcode a parallel top-bar height. - **No Floating Action Buttons (FABs)**: Primary triggers live in the top-bar actions or fixed bottom composer docks. - - **Scaffold Padding Foot-Gun Rule**: Content lambdas must **NEVER** apply `Modifier.padding(paddingValues)` on root children — the scaffold's internal `Box` already handles top-bar offsets. + - **Scaffold Padding Rule**: Do not reapply the supplied `paddingValues` inside the content lambda — the scaffold already handles insets. This includes loading, error, and empty branches. Content-specific spacing is separate from scaffold insets. - **Spacing Scale (`Spacing.kt`)**: - `xs: 2dp`: Hairline gaps, micro dividers. - `sm: 6dp`: Default inner padding, icon-to-text gap. @@ -255,7 +82,7 @@ The app supports 6 dynamic presets managed by `PaletteTemplate.kt` (`Default/Sla - Dropdown pickers take precedence over cycle buttons to ensure all options are discoverable in one tap. ## Elevation & Depth -- **Elevation Language**: Surface luminance layering (`surfaceContainer` tiers) and 1px borders (`outline` / `outlineVariant`) replace heavy drop shadows. +- **Elevation Language**: Surface luminance layering (`surfaceContainer` tiers) and restrained `dp` borders (`outline` / `outlineVariant`) replace heavy drop shadows. - **Flat Surface Rule**: - Chat background is flat `colorScheme.background`. - User message bubbles use solid `colorScheme.primary` (no multi-color gradients). @@ -263,25 +90,24 @@ The app supports 6 dynamic presets managed by `PaletteTemplate.kt` (`Default/Sla ## Shapes - **Corner Radii Hierarchy (`Shapes.kt`)**: - - `0dp` (`rounded.none`): Terminal panels, full-bleed code blocks. - - `4dp` (`rounded.extra-small`): Chips, badges, inline toggles. - - `8dp` (`rounded.small`): Text fields, small cards, code cards. - - `12dp` (`rounded.medium`): Standard cards, list rows, message bubbles, dialogs. - - `16dp` (`rounded.large`): Large cards, bottom sheets. - - `28dp` (`rounded.extra-large`): Feature cards, full-bleed surfaces. - - `9999dp` (`rounded.full`): Interactive filter chips, timeline markers, circular icon buttons. + - `MaterialTheme.shapes.extraSmall`: `4dp`. + - `MaterialTheme.shapes.small`: `8dp`. + - `MaterialTheme.shapes.medium`: `12dp`. + - `MaterialTheme.shapes.large`: `16dp`. + - `MaterialTheme.shapes.extraLarge`: `28dp`. + - Use the existing component's shape contract. Square and circular shapes are component choices, not additional `HermesShapes` tokens. ## Components -- **Top App Bar**: Flat surface, 56dp height, contains title marquee, WS connection status pill, and action buttons. +- **Top App Bar**: Flat surface; reuse `HermesScaffold` and its supported slots instead of duplicating dimensions or controls. - **Chat Timeline**: - **User Bubbles**: Right-aligned, solid primary fill, high-contrast text (`onPrimary`). - - **Assistant Bubbles**: Left-aligned, `surfaceContainer` fill (`#1B2022`), full Markdown renderer support. + - **Assistant Content**: Preserve the active renderer's layout. Full-bleed prose renders on the chat background; do not force every assistant message into a filled card. Reuse the existing Markdown renderer. - **Tool Bubbles**: Expandable rows with tool vector icon, execution state indicator, parameter summary, and full payload bottom sheet. - **Action & Filter Chips**: - Must represent real system state (e.g., active model, selected profile, filter criteria). - Active chips: `primaryContainer` fill with `onPrimaryContainer` text/icon. - - Inactive chips: `surfaceContainerHigh` fill with `text-secondary` text. - - Disabled controls: Visually greyed out with `alpha = 0.38f`. + - Inactive chips: Use the component's Material surface and foreground tokens. + - Disabled controls: Use the component's disabled styling and semantics; do not treat a single alpha as a universal contract. - **Dialogs & Bottom Sheets**: - Standard CRUD flows must provide all 4 operations (Create, Read, Update, Delete) with confirmation gates for destructive actions. - Every managed resource must have a discoverable navigation surface in the UI. @@ -289,17 +115,37 @@ The app supports 6 dynamic presets managed by `PaletteTemplate.kt` (`Default/Sla ## Do's and Don'ts ### Do's -- **DO** use official `androidx.compose.material.icons` vectors for every UI element and status indicator. +- **DO** use vector icons for app-owned controls and status indicators, reusing existing Material icons or project vector assets. - **DO** use `Icons.AutoMirrored.Filled.*` for directional icons (`ArrowBack`, `Send`, `VolumeUp`, `MenuBook`). -- **DO** maintain $\ge 4.5:1$ text contrast and $\ge 3:1$ container contrast across all 6 presets. +- **DO** verify contrast for the actual foreground/background pairs under the accessibility requirements below. - **DO** use Android string `` for count labels (`1 agent` vs `2 agents`). - **DO** grey out disabled controls (`enabled = false`) visibly. - **DO** ensure every created resource has a discoverable UI list or navigation tab. ### Don'ts -- **DON'T** render emoji glyphs (`✅`, `❌`, `⭕`, `🔄`, `⚡`, `🛠`, `📄`) in the app UI — replace them with real vector icons. +- **DON'T** use emoji glyphs as app-owned status icons or controls; use vectors. This does not prohibit emoji in user/assistant messages or other user-authored content. - **DON'T** use Floating Action Buttons (FABs); anchor actions in top bars or fixed docks. - **DON'T** apply `Modifier.padding(paddingValues)` inside `HermesScaffold` content lambdas. - **DON'T** use gradient backgrounds on chat surfaces or message bubbles. - **DON'T** create multi-layer nested cards or unnecessary decorative borders. - **DON'T** use cycle buttons where a dropdown menu provides immediate option visibility. + +## Accessibility and verification requirements + +These are requirements for UI work, not a declaration that the entire app has +passed an accessibility audit. + +- **Touch targets**: Interactive targets must be at least `48dp` in each dimension. A smaller visible icon is fine when its actual hit target meets this requirement. +- **Text scaling**: Use `sp` and semantic typography. Exercise larger system font/display sizes; essential labels, values, and actions must remain readable and reachable rather than clipped into fixed-height containers. +- **TalkBack**: Give meaningful images and icon-only actions localized labels. Decorative icons use `contentDescription = null`; avoid announcing the same label twice. Verify focus order and state announcements in the changed flow. +- **RTL**: Use start/end spacing and auto-mirrored directional icons where appropriate. Check Arabic layout, mixed-direction text, and action order; do not mirror non-directional icons arbitrarily. +- **Contrast**: Target at least `4.5:1` for normal text, `3:1` for large text, and `3:1` for meaningful non-text indicators/control boundaries against adjacent colors. This is not a blanket requirement that every decorative container contrast with every other container by `3:1`. +- **Themes**: Check relevant light/dark presets and dynamic colors. Do not rely on color alone to convey selection, failure, or success. +- **Narrow layouts**: Check long localized labels, keyboard-open states, and scrolling at small widths; density must not hide essential actions. + +[ThemePaletteTest.kt](app/src/test/java/com/m57/hermescontrol/theme/ThemePaletteTest.kt) +currently guards error slot pairs, full-bleed prose/header contrast, and theme-mode +invariants. Those tests are useful but do not establish contrast or accessibility +compliance for every rendered component, alpha blend, or dynamic palette. +Record device/emulator steps and observed results for changed UI; explicitly name +any gate that could not be verified. diff --git a/README.md b/README.md index 15a08094..8fd98e94 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ ## Overview -**Hermes Mobile** is the native Android client for [Hermes Agent](https://hermes-agent.nousresearch.com). It connects securely to your local Hermes gateway (REST API and WebSocket TUI Gateway) over LAN, giving you pocket control over your AI assistant. +**Hermes Mobile** is the native Android client for [Hermes Agent](https://hermes-agent.nousresearch.com). It connects to your Hermes gateway (REST API and WebSocket TUI Gateway), giving you pocket control over your AI assistant. Plain HTTP/WS connections are intended for trusted networks only; authentication does not encrypt the transport. --- @@ -67,17 +67,23 @@ If Hermes Mobile is useful to you, consider supporting its development on [Ko-fi - **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. +- **Theming:** 6 built-in color presets (Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord) 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. --- ## Quick Start +1. Install Hermes Mobile from [F-Droid](https://f-droid.org/packages/com.m57.hermescontrol/) or download the APK from the [latest GitHub release](https://github.com/Hy4ri/hermes-mobile/releases/latest). +2. Start your Hermes dashboard on a host reachable from your phone. +3. Follow [Authentication](#authentication) below to connect. Use a trusted network for plain HTTP/WS connections. + +## Build from source + ### Prerequisites - **JDK 21+** (required for Kotlin compilation and the Gradle toolchain) -- **Android Studio** (Ladybug+) or a **Nix** development environment +- An **Android SDK** matching the compile SDK in [`app/build.gradle.kts`](app/build.gradle.kts), available through Android Studio or the **Nix** development environment. Set `ANDROID_HOME` or configure `sdk.dir` in your local `local.properties`. ### Build & Deploy @@ -160,7 +166,7 @@ app/src/main/java/com/m57/hermescontrol/ ├── 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) +├── ui/ # Compose feature screens + common components (HermesScaffold, StateViews) ├── util/ # CronExpressionFormatter, LocaleContextWrapper └── Navigation*.kt # Navigation3 wiring, keys, screen registry, controller ``` @@ -169,17 +175,19 @@ app/src/main/java/com/m57/hermescontrol/ ## Tech Stack -- **Language:** Kotlin 2.4.10 with KSP 2.3.10 compiler plugin -- **UI & Layout:** Jetpack Compose (BOM 2026.03.01) & Material 3 / Material You +- **Language:** Kotlin with KSP +- **UI & Layout:** Jetpack Compose & Material 3 / Material You - **Navigation:** Navigation3 (Compose-first Routing) -- **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 +- **Networking:** Retrofit, OkHttp, Kotlinx Serialization +- **Database:** Room with SQLCipher 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 +- **Theming:** Built-in presets + Material You dynamic colors; see [Themes](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md) +- **Image Loading:** Coil +- **Testing:** JUnit, MockK, Turbine, Espresso, Compose UI testing - **Formatting:** `ktlint` 1.8.0 style rules (checked automatically in CI) +Dependency versions are maintained in [`gradle/libs.versions.toml`](gradle/libs.versions.toml); build configuration and dependency scopes live in [`app/build.gradle.kts`](app/build.gradle.kts). + --- ## Contributing @@ -190,7 +198,7 @@ Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for ou Help translate Hermes Mobile into your language on [Hosted Weblate](https://hosted.weblate.org/projects/hermes-mobile/hermes-mobile/)! -For developer-specific details, code conventions, and project architecture notes, refer to [AGENTS.md](AGENTS.md). +For operational conventions and architecture notes, refer to [AGENTS.md](AGENTS.md). [DESIGN.md](DESIGN.md) defines visual and interaction requirements; [THEMES.md](app/src/main/java/com/m57/hermescontrol/theme/THEMES.md) explains theme implementation. --- diff --git a/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md b/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md index 73f63da2..01f7dd11 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md +++ b/app/src/main/java/com/m57/hermescontrol/theme/THEMES.md @@ -1,6 +1,10 @@ # Themes -How theming works in Hermes Control, and how to add or edit a theme. +How theming works in Hermes Mobile, and how to add or edit a theme. + +This is the implementation guide. [DESIGN.md](../../../../../../../../DESIGN.md) +defines visual and interaction requirements; the Kotlin sources define the actual +tokens. When changing tokens or behavior, update the relevant documentation too. ## Where themes live @@ -120,4 +124,4 @@ active preset via `LocalHermesStatusColors`. - ktlint 1.8.0 is enforced in CI. Run `./gradlew ktlintCheck` before committing (the gradle task is the gate, not the standalone `ktlint` binary). - Import order is ASCII-lexicographic (uppercase before lowercase). -- Every change goes through a PR — never push directly to `main`. +- Every change goes through a PR targeting `dev` — never push directly to `main` or `dev`.