Commit Graph
38 Commits
Author SHA1 Message Date
Hy4ri 5ba266cefd fix(chat): resolve compilation errors and formatting violations 2026-06-20 11:12:34 +03:00
Hy4ri d0eedf053f refactor(chat): stabilize streaming, session loading, and connection status
Optimize streaming message updates, correct session load merging, and derive connection state reactively. Add reconnection UI banner.
2026-06-20 11:04:56 +03:00
M57andM57 f5edf1569c fix(#141): notification tap opens original session instead of new session (#147)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 10:10:10 +03:00
M57andM57 ae62b05e6d fix(#144): add timestamp to tool/command message bubbles (#145)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 10:01:44 +03:00
M57andM57 7248f66abf feat(#136): visible copy button overlay on long-press of message bubbles (#143)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 09:46:10 +03:00
M57andronia 261ee6379a feat(#132): dynamic session title with ellipsis truncation (#139)
Co-authored-by: ronia <ronia@m57>
2026-06-20 02:37:55 +03:00
M57andronia 913c6fa878 fix(#134): remove settings button from chat top bar (#138)
Co-authored-by: ronia <ronia@m57>
2026-06-20 02:28:36 +03:00
M57andronia 27ac4d72a4 fix(#133): only show status dot red when offline, hide when connected (#137)
Co-authored-by: ronia <ronia@m57>
2026-06-20 02:25:29 +03:00
M57 61d49d2355 fix(chat): don't reload cached msgs on SESSION_RESUME ack 2026-06-20 00:54:16 +03:00
M57 0fabad820d fix(chat): fix old session resume losing server session ID
* fix(chat): reset agent typing status when resuming old sessions

* fix(chat): fix race condition in websocket request registration

* test(chat): update mocks for new send/sendMessage onSent callback

* style(chat): use named onSent param instead of trailing lambda

* style(chat): fix import ordering for ConcurrentHashMap

* test(chat): fix remaining 2-arg verify calls for send()
2026-06-20 00:40:16 +03:00
M57 46b24593f9 feat(#58): implement skill markdown/yaml editor UI (#121) 2026-06-19 23:58:34 +03:00
Hy4ri 45f78443ab feat(#60): format tool output JSON for readability 2026-06-19 23:28:16 +03:00
Hy4ri 508aea8bfa fix(#79): centralize retrofit error handling with NetworkResult 2026-06-19 22:58:33 +03:00
M57andM57 43df3c6e73 fix(#64): call wsClient.disconnect() directly in onCleared() instead of launching on cancelled scope (#94)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:18:04 +03:00
M57andM57 abcec14cb5 feat(#76): add testTag identifiers to composable elements for UI test targeting (#98)
Adds testTag modifiers to key interactive elements across the core
composables, enabling Espresso/Compose UI tests to locate them:

Navigation.kt  — 'nav_*' on bottom bar items, 'drawer_*' on drawer items
HermesScaffold — 'back_button', 'menu_button', 'refresh_button'
StateViews.kt  — 'loading_state', 'loading_spinner', 'error_retry_button',
                 'empty_state_action'
ChatScreen.kt  — 'chat_input', 'send_button', 'interrupt_button'

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-19 22:15:53 +03:00
M57andM57 cc52442be3 fix(#65): request POST_NOTIFICATIONS runtime permission on Android 13+ (#97)
* 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>
2026-06-19 22:15:47 +03:00
Hy4ri 23ff263641 Fix WebSocket connection stuck on subsequent launches (Issue #45) 2026-06-19 19:56:50 +03:00
Hy4ri 80578b93d6 Render SearchBarRow inside the main Column container below the top app bar 2026-06-19 19:36:20 +03:00
Hy4ri 3734cb361f Use height-constrained CompactSearchInput inside SearchBarRow to prevent topbar height dynamic expansion 2026-06-19 19:17:55 +03:00
Hy4ri 11c5da260d Pin top app bar in ChatScreen and support pinTopBar parameter in HermesScaffold 2026-06-19 19:09:04 +03:00
Hy4ri f7132a76cd Fix chat search breaking vertical scrolling and layout (Issue #44) 2026-06-19 18:59:43 +03:00
Hy4ri 54ebe8714e fix(chat): remove double padding and zero out nested Scaffold window insets 2026-06-19 18:37:48 +03:00
Hy4ri 3a610c7c80 fix(ci): fix trailing spaces and revert visibility/padding changes 2026-06-19 18:23:17 +03:00
Hy4ri 9e3a7ab339 fix: always show bottom bar, resolve chat input double padding, and format codebase 2026-06-19 18:14:41 +03:00
M57andronia 59e0d4f817 feat: search within chat history (Issue #31) (#39)
- 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>
2026-06-19 16:35:07 +03:00
M57 fb605d1bc0 feat: clickable URLs and long-press copy in chat bubbles (Issues #29, #30) (#36)
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
2026-06-19 14:16:56 +03:00
M57andM57 837678c590 fix(#21): remove double spacing above top bar and below chat input (#22)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-18 20:36:56 +03:00
M57 d37de1d223 feat(#11): make HermesScaffold title a @Composable slot, convert ChatScreen (#12)
- 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)
2026-06-18 18:43:45 +03:00
M57andM57 a36be6e432 fix(#4): replace private EmptyState with shared component in ChatScreen
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>
2026-06-18 17:51:52 +03:00
M57andM57 5130c2e308 feat(chat): message persistence + background notifications (#2)
* 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>
2026-06-18 03:23:38 +03:00
M57andRonia a0b4ce8dc0 fix(security,race,ux): close 6 high-priority bugs (B1–B6)
Wave 1 — CRITICAL (token + PII leak):
  B1 (kanban t_afc1d26f): Auth token leaked to logcat via
    HttpLoggingInterceptor.Level.BODY combined with the authInterceptor
    that adds `Authorization: Bearer *** on every REST call. Enabled
    buildConfig=true in app/build.gradle.kts and gate Level.BODY on
    BuildConfig.DEBUG (Level.NONE in release). ApiClient.kt:44-56.
  B2 (kanban t_8884db16): Chat prompts + AI replies leaked to logcat
    via unguarded Log.d in HermesWsClient. Gated the 3 PII-critical
    sites (line 104 outgoing JSON which contains user `text`, line ~123
    connecting URL whose query string carries the auth token, line ~181
    incoming WS payload with AI reply tokens and tool output) with
    BuildConfig.DEBUG. Log.i/.w/.e left unguarded as intentional ops signals.

Wave 2 — HIGH (races + thread-pattern):
  B3 (kanban t_33da8a97): switchSession fired SESSION_RESUME (WS) and
    loadSessionMessages (REST) in parallel. The REST success path did an
    unconditional `_uiState.update { it.copy(messages = chatMessages) }`
    that stomped any in-flight streaming tokens appended via
    WsEvent.MessageToken. ChatViewModel.loadSessionMessages now preserves
    messages with isStreaming=true by re-appending them to the freshly
    loaded history. Bonus side-fix discovered in same function: isLoading
    was never set to false on success or error branches — now explicitly
    cleared on all exit paths.
  B4 (kanban t_2b834d90): HermesWsClient.scheduleReconnect used a raw
    Thread{} with Thread.sleep + a non-atomic check-then-openSocket()
    (TOCTOU). Two reconnect threads spawned by a rapid onFailure/onClosed
    cycle could both pass the gate and both call openSocket(), the second
    overwriting `webSocket` without closing the first — orphaned socket
    listener leak. Flaky networks multiplied daemon thread count. Replaced
    with a single cancellable coroutine Job keyed to wsScope
    (Dispatchers.IO + SupervisorJob). disconnect() now cancels any pending
    reconnect so a delayed openSocket() can't fire after explicit close.

Wave 3 — MEDIUM (UX gaps + persistence):
  B5 (kanban t_2322818d): LogsScreen rendered a blank black Box when
    state.logs was empty && !isLoading && errorMessage == null. Added an
    explicit empty-state branch rendering "No logs available" in
    onSurfaceVariant, centered. Mirrors sibling pattern in
    PairingScreen.kt:200-219 and WebhooksScreen.kt:171-190.
  B6 (kanban t_86e9be9b): Theme preference was in-memory only — lost on
    every cold start. Verification confirmed three gaps: (1)
    SettingsViewModel.loadSettings() didn't restore themePreference from
    storage; (2) SettingsViewModel.save() didn't persist it; (3)
    MainActivity called `HermesControlTheme {}` with no themePreference
    arg so even if storage had worked, the app wouldn't apply it at
    startup. Added getThemePreference/setThemePreference in AuthManager
    (EncryptedSharedPreferences round-trip, validate stored enum name with
    runCatching to handle forward-incompatible strings), wired
    SettingsViewModel init + save, and passed the stored preference to
    HermesControlTheme(themePreference = AuthManager.getThemePreference())
    at startup.

Verification:
  - ktlintCheck: BUILD SUCCESSFUL in 7s (ktlint app/src/**/*.kt — clean)
  - ./gradlew test: NOT RUN on this host (no Android SDK at
    $ANDROID_HOME); deferred to the CI workflow at
    .github/workflows/android.yml. Same limitation applies to
    assembleRelease. Symbol-resolution reasoning: app/build.gradle.kts:10
    declares namespace=com.m57.hermescontrol so AGP auto-generates the
    BuildConfig class at package com.m57.hermescontrol — matches the new
    imports in ApiClient.kt and HermesWsClient.kt.

Kanban tracking (native Hermes kanban, board slug "hermes-mobile"):
  B1 t_afc1d26f  done   - Auth token leak via Level.BODY
  B2 t_8884db16  done   - Chat PII via unguarded Log.d
  B3 t_33da8a97  done   - switchSession race stomps streaming tokens
  B4 t_2b834d90  done   - WS reconnect TOCTOU + unbounded Thread{}
  B5 t_2322818d  done   - LogsScreen missing empty-state
  B6 t_86e9be9b  done   - Theme preference not persisted
  B7 t_74f3b383  ready  - (deferred) NavigationController.goBack underflow
  B8 t_39a149f0  ready  - (deferred) KanbanBoardsResponse.current nullable
  B9 t_4b1465cf  ready  - (deferred) E2eIntegrationTest base64 no-op via Mockk
  B10 t_81ef4203 ready  - (deferred) ChannelsScreen form state on scroll
  B11 t_3325498b ready  - (deferred) TEST_READY/TEST_INFRA stale (28 vs 123)
  B12 t_674007f5 ready  - (deferred) Hardcoded http:// (LAN assumption)
  B13 t_179009d5 ready  - (deferred) SystemViewModel silent doctor catch

Total: 8 files changed, +144 insertions, -26 deletions.

Co-Authored-By: Ronia <ronia@hermes>
2026-06-18 02:17:11 +03:00
Hy4ri bd439ba674 style: modernize UI aesthetics and replace app launcher icons 2026-06-17 00:42:14 +03:00
Hy4ri 3830236585 feat: slash command autocomplete, dynamic theme colors, interactive model selection & drawer icons 2026-06-16 18:15:01 +03:00
Hy4ri 69f36b1606 fix: resolve double-padding on chat screen when keyboard is opened 2026-06-16 00:34:45 +03:00
Hy4ri 48bf0a1ffc feat: add full chat slash commands support (/help, /status, /sessions, /stats) 2026-06-16 00:32:10 +03:00
Hy4ri 21a020c87e feat: load message history when switching or resuming a session 2026-06-16 00:27:00 +03:00
Hy4ri c1d0956a1d feat: add button to create new chat directly in ChatScreen top bar actions 2026-06-16 00:23:57 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00