* feat(chat): message persistence + background notifications
Two critical gaps for daily-driver use:
1. Message persistence (Room database):
- New ChatMessageEntity / ChatMessageDao / HermesDatabase layer
- Every user message, streaming token, tool result, and assistant
reply is written to a local Room DB as it arrives
- switchSession loads cached messages instantly from Room before
the REST call returns — no more blank screen while loading
- Session resume also loads from cache first
- Messages survive app kills, process death, and configuration
changes
- ChatViewModel changed from ViewModel() to AndroidViewModel(app)
to access the database context
2. Background notifications (foreground service):
- ChatNotificationService keeps the WebSocket alive while the app
is backgrounded and posts a high-priority notification when a
MessageComplete or ClarifyRequest event arrives while the app
is not in the foreground
- Lifecycle wired via LifecycleEventObserver in ChatScreen:
ON_STOP starts the service, ON_START stops it
- Tapping the notification returns to the chat
- New permissions: POST_NOTIFICATIONS, FOREGROUND_SERVICE,
FOREGROUND_SERVICE_DATA_SYNC
Build changes:
- Added Room 2.7.1 + KSP to version catalog
- Added WorkManager 2.10.0 (reserved for future polling fallback)
- Updated ChatViewModelTest to mock HermesDatabase + use
AndroidViewModel constructor
* fix(build): use KSP 2.3.9 (new versioning scheme for Kotlin 2.3.x)
* fix(build): add Room schemaDirectory (required by Room Gradle plugin)
* fix(compile): add else branch to WsEvent when expression (exhaustiveness)
* ci(release): add release workflow + R8 minification + proguard rules
New .github/workflows/release.yml:
- Triggers on tag pushes (v*) and manual workflow_dispatch
- Runs full CI (ktlint + lint + test) before building
- Builds assembleRelease with version injection from tag
- Creates a GitHub Release with auto-generated changelog
- Attaches the release APK to the release
Build changes:
- Release builds now use R8 minification (isMinifyEnabled=true)
and resource shrinking (isShrinkResources=true) for smaller APKs
- versionCode/versionName injectable via -PversionCode / -PversionName
Gradle properties (passed by CI from the git tag)
- Local dev builds default to versionCode=1, versionName=1.0-dev
ProGuard rules:
- Keep all data model classes (Gson reflection)
- Keep Room entities and RoomDatabase subclasses
- Keep OkHttp/Retrofit interface methods
- Suppress warnings for Compose, Security Crypto, Coroutines
* feat(ci): release workflow + proguard rules + test fix
Release workflow (.github/workflows/release.yml):
- Triggers on tag push (v*) or manual workflow_dispatch
- Two jobs: ci (ktlint+lint+test) → build-release
- Builds a minified, shrunk release APK with version from tag
- Creates a GitHub Release with auto-generated release notes
- Attaches the APK as a release asset
Build config:
- versionCode/versionName now injected from CI via -P flags
- Release build enables isMinifyEnabled + isShrinkResources
- proguard-rules.pro: keep rules for Gson models, Room entities,
OkHttp/Retrofit interfaces, Coroutines, Compose, BuildConfig
Test fix:
- ChatViewModelTest: use coEvery for suspend DAO methods instead
of every (compile error — suspend functions need coEvery)
- Added stubs for all suspend DAO methods used by the ViewModel
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(ui): modern theme, sectioned drawer, shared components (v1)
Foundation for the modern redesign — fixes the biggest UX problems
(that vague "better ui ux" ticket) without rewriting all 20 screens
at once. This puts the infrastructure in place and modernizes the
highest-traffic screens.
Theme (
- Disable Material You dynamic color by default — brand identity
(Voltage Purple) wins. Can opt back in via HermesControlTheme.
- 3-step elevation ladder (surface / surfaceVariant / surfaceHigh)
with subtle blue tint so dark mode stops reading as flat black.
- Full Material 3 type hierarchy — every style explicitly tuned,
tighter letter spacing, comfortable line heights.
- New Shapes.kt (xs→xl rounded corner tokens) and Spacing.kt
(xs/sm/md/lg/xl/xxl spacing tokens) — no more ad-hoc dp values.
Navigation (
- Sectioned drawer: 17 items were dumped at one indent level with
duplicate icons (Channels & Chat both used the Chat icon!). Now
grouped into 4 logical sections — Converse / Automate / Configure
/ Inspect — with section headers and sane per-item icons.
- Settings added to the drawer (was previously hidden behind
ChatScreen's gear icon — bad IA).
- gesturesEnabled check collapsed from an 18-term OR chain into a
single set-membership test.
Common components (
- HermesScaffold: shared drawer-aware Scaffold + TopAppBar with
slots for refresh / actions. Removes the per-screen duplication
seen in SkillsScreen, CronJobsScreen, ChannelsScreen, etc.
- StateViews: LoadingState / ErrorState / EmptyState / list content
padding tokens — every list screen now renders loading/error/
empty identically instead of ad-hoc per screen.
Modernized screens:
- ConnectScreen: hero brand badge (was a lone "⚡" glyph), pairing
as the primary path, manual token/host/port collapsed behind a
toggle (was always-visible clutter), Card-in-Card nesting removed.
- SkillsScreen: now uses HermesScaffold + EmptyState + ErrorState +
LoadingState. Adds a search field, leading icon per row, and a
category sublabel.
Follow-up commits will migrate the remaining ~15 list screens onto
HermesScaffold; this commit stops at the foundation + the two screens
that see the most daily use.
Verified: ktlint clean across the entire tree.
Co-Authored-By: Ronia <ronia@m57>
* fix(imports): add missing safeDrawingPadding + dp imports
CI caught three compile errors that ktlint doesn't flag (unresolved
references are not lint errors, they're compile errors):
- Navigation.kt:239 — Modifier.safeDrawingPadding() import was
dropped when the file was rewritten for the sectioned drawer.
- SkillsScreen.kt:117 — `16.dp` and `12.dp` references had no
`androidx.compose.ui.unit.dp` import after the file was rewritten
onto HermesScaffold.
No code changes — pure import fixes.
Co-Authored-By: Ronia <ronia@m57>
* ci: split monolithic job into 4 parallel jobs + summary
Before — one job ran ktlint → lint → test → build sequentially. If
ktlint failed you waited ~30s to find out, and the test/build stages
never even started (no useful artifacts uploaded on partial failure).
After — 4 jobs run in parallel on every push and PR:
1. ktlint (fast feedback, ~30s)
2. android-lint (parallel, includes compileDebugKotlin)
3. unit-tests (parallel, JUnit 5 + mockk)
4. build (gated by 1+2+3, only runs if all pass)
Plus a 5th `ci-summary` job that aggregates results so branch
protection only needs a single status check configured.
Other improvements:
- concurrency cancel-in-progress — new commits on the same ref
cancel the stale run instead of piling up.
- timeout-minutes on every job — no more hung runners burning minutes
on a stuck Gradle daemon.
- workflow_dispatch — can trigger manually from the Actions tab.
- Per-job artifact uploads with `if: always()` — reports upload even
when the job fails, so you can download test/lint reports to debug.
- Removed the monolithic "verification-reports" artifact that tried
to upload three unrelated paths and failed silently with "No files
were found" when an early step bailed.
Co-Authored-By: Ronia <ronia@m57>
* ci: silence ktlint artifact-not-found annotation
Adds `if-no-files-found: ignore` to the ktlint artifact upload step
because ktlint 1.2.1 does not produce HTML reports on success — only
on failure. The previous config `if-no-files-found: warn` (default)
emitted a noisy annotation on every green run.
Co-Authored-By: Ronia <ronia@m57>
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Ronia <ronia@m57>