* fix(#65): request POST_NOTIFICATIONS runtime permission on Android 13+
On API 33+ the manifest declaration alone is not enough — the app must
request POST_NOTIFICATIONS via ActivityResultContracts.RequestPermission
at runtime before it can show any notification, including the foreground
service notification from ChatNotificationService.
- Added rememberLauncherForActivityResult for POST_NOTIFICATIONS
- Added LaunchedEffect(Unit) that fires the permission request on first
composition when running on API 33+ and permission is not yet granted
- The next lifecycle event (ON_STOP → NotificationHelper.start) picks
up the permission if granted
* fix: add missing import android.os.Build for POST_NOTIFICATIONS permission check
* fix: add missing import for rememberLauncherForActivityResult (androidx.activity.compose)
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Notification smallIcon must use a simple mono vector drawable, not a
launcher mipmap. Launcher icons render broken in the status bar on
many devices.
- Added app/src/main/res/drawable/ic_notification.xml (simple info icon)
- Updated both showReplyNotification and buildForegroundNotification to
reference R.drawable.ic_notification instead of R.mipmap.ic_launcher
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* ci: prevent concurrency cancellation on rapid main merges
The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.
Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).
* refactor(#82): remove dead SkeletonList/SkeletonCard composables
* fix(#72): echo server's WebSocket close code instead of hardcoded 1000
* fix(#75): replace non-null assertion on simpleName with safe mapNotNull
---------
Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* ci: prevent concurrency cancellation on rapid main merges
The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.
Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).
* refactor(#82): remove dead SkeletonList/SkeletonCard composables
* fix(#72): echo server's WebSocket close code instead of hardcoded 1000
---------
Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* ci: prevent concurrency cancellation on rapid main merges
The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.
Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).
* refactor(#82): remove dead SkeletonList/SkeletonCard composables
---------
Co-authored-by: ronia <ronia@m57>
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
The concurrency group used github.ref which is static for main
(refs/heads/main). When multiple PRs merged rapidly (~2 min apart),
each merge commit cancelled the previous CI run before it could finish.
Fix: use github.sha for push events to main (unique per commit) while
keeping the ref-based group for PR branches (cancels on new commits to
the same PR branch).
Co-authored-by: ronia <ronia@m57>
- Add search icon toggle in top bar with animated search field
- Real-time message filtering on search query match
- Match counter with previous/next navigation arrows
- Case-insensitive text highlighting in user and assistant bubbles
- Dismiss search restores full message list
- Empty state for no search results
Co-authored-by: ronia <ronia@m57>
Add clickable URL detection with primary color + underline styling and browser launch via UriHandler. Add long-press to copy raw message text to clipboard for both user and assistant chat bubbles.
Fixes#29, Fixes#30
## What
Adds a new Navigation Bar section to Settings where users can customize which items appear in the bottom navigation bar.
## Features
- Pick from any of the 19 drawer screens (max 5 items)
- Remove items with a close/delete button
- Items persist via SharedPreferences (CSV format)
- Invalid names are silently dropped
- Existing screens and auth-aware filtering respected
Closes#23
- HermesScaffold: added isRefreshing param, wraps content in PullToRefreshBox when onRefresh is set
- All 16 data screens: pass isRefreshing = state.isLoading
- Initial loading only shows full-screen LoadingState when data is empty (no spinner over existing content)
- AchievementsScreen already had the correct pattern, just needed isRefreshing param
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
- HermesScaffold: title now @Composable () -> Unit (M3-idiomatic)
- HermesScaffold: added modifier and snackbarHost parameters
- Updated all 18 callers from title="X" to title={Text("X")}
- ChatScreen: converted from raw Scaffold to HermesScaffold
- Removed unused Menu icon import (ktlint fix)
* refactor(#5): convert SettingsScreen to use HermesScaffold
Replace raw Material3 Scaffold + manual TopAppBar with the shared
HermesScaffold wrapper. Removes ~30 lines of duplicated boilerplate
and gives SettingsScreen scroll-aware top bar behavior.
Closes#5
* fix(#5): restore ExperimentalMaterial3Api import for SegmentedButton
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Remove ~30 lines of duplicated code by using the shared EmptyState
from ui/common/StateViews.kt instead of the private version.
Closes#4
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Complete UI/UX rewrite of hermes-mobile with Material You design system.
Phase 1: Color/Theme/Type/Shapes/Spacing/Motion tokens
Phase 2: HermesScaffold, StateViews (skeleton/empty/error), SearchBar, StatusBadge, ToggleRow
Phase 3: Bottom nav + drawer navigation with NavigationController
Phase 4: All 17 screens rewritten with HermesScaffold and Material You patterns
Closes PR #3.
* 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>