Commit Graph
48 Commits
Author SHA1 Message Date
M57 f5eaecbbef perf: optimize screen load times with progressive un-gating and SWR caching (#1192) 2026-09-18 23:25:30 +03:00
M57 65c8e07162 fix(ui): keep scroll position when focusing scrolled editors (#1116) 2026-09-15 05:13:56 +03:00
M57 d774a63663 feat(plugins): browse and install official plugins from catalog (#1088) (#1096) 2026-09-12 22:32:52 +03:00
Hy4ri e6e79be10d feat(common): add ConfirmDialog and safeLaunchAction helper
Add reusable ConfirmDialog with destructive action styling.
Add safeLaunchAction to ViewModelExt for mutating calls with unit tests.
2026-09-11 18:24:00 +03:00
M57 850d01ca89 fix(update): bind live download state to dialog and show real-time progress 2026-09-06 20:59:39 +03:00
M57andM57 cfe3e736fd chore: upgrade ktlint to 1.8.0 and format codebase (#1024)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-31 12:27:40 +03:00
M57andM57 f04d878358 feat(bots): add multi-agent group chat collaboration room (#998)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-28 17:50:37 +03:00
M57andM57 14fde03083 feat(image): migrate to Coil 3.6.0 with OkHttp network fetcher (#996)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-27 15:13:18 +03:00
M57 6af349cdf2 feat(ui): add reusable Material3 BotAvatar component (#974) 2026-08-27 01:49:08 +03:00
M57andM57 1f1ebf3597 feat(update): redesign in-app updater with smart cooldown, release notes, and in-place dialog (#971)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-26 07:42:41 +03:00
M57 ece0288ca8 feat(update): surface backend update receipt in action popup (#958) (#964)
Adopt GET /api/hermes/update/receipt and append its summary to the
end of the hermes-update action progress popup after it finishes.
Additive only: existing checkHermesUpdate + connection-retry polling
is untouched. 404 (old backend) is handled gracefully.

- add UpdateReceiptResponse data model matching backend schema
- add getUpdateReceipt() to HermesApiService
- ActionProgressController gains trailingLines + pushTrailingLines
- ActionProgressDialog renders trailingLines after the log
- SystemViewModel fetches receipt on action finish and formats it
- extract pure formatUpdateReceiptLines for mockk-free testing
2026-08-24 01:35:59 +03:00
M57andM57 85087c70c7 feat: surface /api/status memory+disk pressure on connect/status screens (#903) (#908)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 13:00:08 +03:00
M57andM57 daf33dcb4c feat(update): notify about app updates on launch (chat banner) (#891)
Fires one silent update check per installed version at app start
(UpdateNoticeManager, release builds only) and shows a non-blocking
banner on the chat screen when a newer release exists. The About tab
adopts the launch result instead of re-pinging GitHub; dismissals
return on the next launch via the persisted latest tag.

Closes #890

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 01:08:42 +03:00
M57andM57 96e43f354f feat(system): update progress popup with live action log (issue #863) (#872)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-10 16:16:34 +03:00
M57andM57 a9cb89a925 feat(cron): monitor-mode jobs (monitor_script/monitor_url) + blocked_config status (issue #861) (#871)
- CronJob model gains optional monitor_script/monitor_url (additive-safe)
- Job editor: Monitor mode section (off/script/url) with XOR source clearing;
  disabled while no-agent is set (backend invariant); new jobs apply monitor
  via create -> follow-up PUT with rollback delete on failure (the REST create
  model drops the fields)
- Job list/details: blocked_config renders as a distinct error badge
  ('Blocked (config)'), no_change as informational; monitor source shown in
  run details

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-10 15:35:05 +03:00
M57andM57 f34bfa7cf8 feat(ws): consume gateway change events (issue #784) (#837)
* feat(ws): consume gateway change events (cron/sessions/platforms/pairing) for targeted refresh (issue #784)

* test(chat): complete catch-all request stub — kill frozen-collector capture race (slash-focus flake, 4645a2b CI)

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-07 13:48:17 +03:00
M57andM57 7d00d7b81b feat(memory): dedicated Memory screen (moved out of System tab) (#835)
* feat(memory): dedicated Memory screen — providers, builtin files & reset (moved from System tab)

* feat(memory): move self-improvement & learning activity section from System tab to Memory screen

* fix(memory): remove top padding, single-line reset buttons, providers spacing

* fix(memory): tighten provider card padding

* fix(memory): distinct AutoStories icon for Memory tab

* chore: ignore local debug apk

* feat(ui): fresh icon set for drawer screens

* feat(ui): custom Neurology icon for Memory tab

* fix(ui): theme-driven fill for NeurologyIcon (checkColorLiterals)

The custom Neurology vector used SolidColor(Color.Black), tripping the
hardcoded-color guard (issue #622). The fill only contributes alpha —
Icon()'s tint replaces the RGB at draw time, mirroring Material's own
icon set — so the literal belongs in theme/ like every other hardcoded
color: new IconDefaultFill token, scoped there by the guard's design.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-07 04:18:50 +03:00
M57andM57 4645a2b6e7 test: de-poison unit suite — drop static Dispatchers mocks, kill leaked WS timers (#833)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-07 04:18:14 +03:00
M57andM57 509a018434 refactor(ui): extract shared ExposedDropdownField (issue #796) (#815)
DropdownField (ConfigScreen) and PluginProviderDropdown (PluginsScreen)
were byte-for-byte identical. Extract into ui/common/ExposedDropdownField
and route both call sites to it. No behavior change.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-04 20:39:23 +03:00
M57 5c1492a80e feat(chat): add animated GIF rendering with tap-to-play animation (#721) (#731)
* feat(chat): add animated GIF rendering with tap-to-play animation

Closes #721

* fix(#721): add missing GifImageThumbnail component and format ktlint

* fix(analytics): resolve ktlint indentation error in itemsIndexed

* feat(chat): add interactive play/pause toggle for animated GIFs

* fix(chat): control GIF playback via Animatable start and stop
2026-07-27 18:32:45 +03:00
M57 426f14550e feat(ui): redesign history prune button and compact search bar (#702) 2026-07-24 00:39:41 +03:00
M57andM57 a62bc7e16c refactor: remove device Pairing feature entirely (issue #645) (#650)
* refactor: remove device Pairing feature entirely (issue #645)

- Delete Pairing.kt model + pairing API endpoints (get/approve/revoke/clear)
- Delete PairingScreen/PairingViewModel + PairingCodeEntryScreen/ViewModel
- Remove NavKeys, ScreenRegistry drawer entry, Navigation.kt entry + Landing pairing button
- Remove pairing string resources (en + ko); keep connect_* config-parse strings
- Keep Telegram onboarding pairing_id flow (separate concept, per issue)
- Fix related doc comments + remaining test reference

* test: fix racy CONNECTED read in gated-mode WS test (issue #635 test)

testGatedMode_attemptsTicketFetchWithoutBareCookie asserted CONNECTED
immediately after the server-side onOpen latch fired, racing the client
handshake (WsListenerImpl sets CONNECTED on receipt of the 101). Await the
real status transition via connectionStatus.first{==CONNECTED} like the
other connect tests do. No product behavior changed.

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 23:23:29 +03:00
M57andHy4ri 0018e5d172 feat(#617): skeleton loaders instead of bare spinner on list screens (#625)
Adds SkeletonListState to StateViews.kt — an animated shimmer
placeholder list whose geometry matches a typical list row (avatar circle
+ title line + subtitle line). The shimmer colors derive from
MaterialTheme.colorScheme.surfaceVariant (theme-aware, works on
light/dark/AMOLED/Material You). Sweeps horizontally via an
infiniteTransition tween — no new dependencies, pure Compose foundation
animations.

Wired SkeletonListState into 22 list-loading call sites across 22 screens:
skills, cron, plugins, mcp, toolsets, webhooks, system, sessions (x2),
config (x2 — list + YAMLEditor), model, profiles, keys, channels,
achievements, gateways, kanban, logs, analytics, providers,
processes, hub skills tab, installed skills tab (list branch).

Preserved LoadingState() (centered spinner) where the issue explicitly
calls for it:
- PairingScreen (one-shot auth flow)
- SkillEditorDialog / ModelPickerDialog / CronJobEditorDialog interiors
- SkillPreviewDialog and SkillEditorDialog inline-loading branches in
  SkillsScreen.kt (lines 790, 914)
- InstalledSkillsTab dialog-interior branch (line 393)

SkeletonListState testTag: "skeleton_list_state".

Verified: ktlintCheck, assembleDebug, testDebugUnitTest all green locally.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
2026-07-18 15:12:21 +03:00
M57andHy4ri 0a0b612d3a refactor(#619): kill DRAWER_GESTURE_SCREENS — screen-owned drawer gestures via HermesScaffold (#624)
Replaces the fragile DRAWER_GESTURE_SCREENS global set + defensive
LaunchedEffect(snapTo(Closed)) + closeDrawer callback workaround with a
single source of truth: each screen declares its drawer-gesture preference
via HermesScaffold's new drawerGesturesEnabled parameter (default true).

Architecture:
- DrawerGestureController holds the active gesture state. HermesScaffold
  reconciles each screen's preference into it via SideEffect; when a screen
  opts out while the drawer is open, the controller closes the drawer itself.
- LocalDrawerGestureController CompositionLocal flows the controller from
  MainNavigation down to ModalNavigationDrawer, which reads .enabled and
  passes it to Material's gesturesEnabled parameter.
- DisableDrawerGestures() helper for entry screens that don't use
  HermesScaffold (Landing, AuthLogin, PairingCodeEntry).
- All 6 Settings drill-down sub-pages pass drawerGesturesEnabled = false.
- NavigationController.closeDrawer removed; navigateTo no longer needs a
  synchronous drawer-dismiss hook.

Removes: DRAWER_GESTURE_SCREENS set, both LaunchedEffect(gesturesEnabled)
and LaunchedEffect(drawerGesturesEnabled) blocks, drawerGesturesEnabled
local state, closeDrawer callback on NavigationController.

Fixes the recurring drawer-scrim stuck-open bug class (PRs #445, #454, #455).

Verified: ktlintCheck, assembleDebug, testDebugUnitTest all green locally.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
2026-07-18 14:25:23 +03:00
M57andM57 3030e431f2 feat: auto-scroll long chat title once on load + on tap (#612)
* feat: auto-scroll long chat title once on load + on tap

* fix: resolve state delegate + suspension compile errors in AutoScrollingTitleText

* fix: use Int scroll offset for animateScrollTo

* fix: measure real container width for title marquee (was always Infinity)

* fix: ease the title marquee scroll (FastOutSlowInEasing + glided return)

* fix: address agy review — cancel overlapping scrolls + one-shot on-load guard + clickable on box

* fix(ui): smooth out top app bar collapse transition

Use dynamic layout padding based on scroll offset to prevent blank gap at the top of content when app bar collapses.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-16 21:45:13 +03:00
M57andM57 9e10c813d4 feat(#534): add OAuth provider management screen + credential_warning surfacing (#566)
* feat(#534): add OAuth provider management screen + credential_warning surfacing

* style(#534): fix ktlint import-ordering in ProvidersViewModel

* fix(#534): address agy review — terminal DONE/ERROR dialog states + PKCE browser launch

* fix(#534): add Done/Close dialog strings

* fix(#534): address agy review - terminal DONE/ERROR dialog states, real PKCE browser launch, clear flow state

---------

Co-authored-by: M57 <m57@hermes-control.app>
2026-07-12 21:10:11 +03:00
M57 cbbda06157 feat(#500): tap-to-reveal detail dialog for truncated item descriptions (#501)
* feat(#500): tap-to-reveal detail dialog for truncated cards

Add shared DetailDialog composable and wire tap-to-open full metadata
dialogs across Skills (installed + hub), MCP servers, Plugins, Toolsets,
and Webhooks screens. Action buttons stay on the card (sticky); tapping
the card body reveals untruncated description + key metadata.

Refs: #500

* fix(#500): resolve compile errors from missing runtime imports + dialog scope

- add missing androidx.compose.runtime.mutableStateOf/remember imports
  in InstalledSkillsTab + HubSkillsTab (caused 'Unresolved reference'
  cascade on compileDebugKotlin)
- move PluginsScreen detail dialog inside the composable function scope
  (was nested in OrphanPluginCard -> showDetail unresolved)
- DetailDialog rows type: Pair<String?,String?> -> Pair<String,String?>
  (key always non-null; fixes type-inference failure)
- drop unnecessary safe call on non-null label

Verified locally with the Android SDK:
  ./gradlew :app:compileDebugKotlin  -> BUILD SUCCESSFUL
  ./gradlew :app:testDebugUnitTest   -> BUILD SUCCESSFUL

Refs: #500

* refactor(#500): extract toDetailRows() mappers + semantic status tones; wire SkillsScreen hub browse

- add DetailRows.kt with @Composable extension mappers for Skill/HubSkill/
  McpServer/PluginInfo/Toolset/WebhookSubscription -> List<DetailRow>
- DetailDialog now renders status rows via LocalHermesStatusColors (review #2):
  SUCCESS/NEUTRAL/WARNING/ERROR/INFO tones instead of plain onSurfaceVariant
- wire SkillsScreen.HubBrowseView/HubSkillCard tap -> DetailDialog (same UX as
  other screens) — user request
- remove JVM-signature-clashing Pair overload; drop unused Color import
- add detail_dialog_trust_level string (HubSkill has no version field)
- verified: compileDebugKotlin + testDebugUnitTest PASS locally w/ ANDROID_HOME

* fix(#500): full hub description in dialog + no name truncation

- HubBrowseView (both SkillsScreen + HubSkillsTab): on card tap, fetch
  previewHubSkill(identifier) to get the FULL SkillContentResponse.content
  (search snippet was truncated); dialog shows full content once loaded,
  with a spinner while previewing. Falls back to snippet if no identifier.
- SkillsViewModel: add hubPreviewContent/isHubPreviewing/hubPreviewError
  state + previewHubSkill()/clearHubPreview() (uses existing preview endpoint)
- DetailDialog: title now wraps fully (removed maxLines=2 ellipsis cap) so
  long skill names are never cut off
- verified: compileDebugKotlin PASS + ktlint clean (local ANDROID_HOME)
2026-07-08 22:50:03 +03:00
M57andM57 b46e0ef46d fix: correct FilterChipRow and EmptyState API mismatches in Skills screen (#466)
* fix: correct FilterChipRow and EmptyState API mismatches + ktlint fmt

* fix: add missing Save import, fix verticalArrangement double-wrapping

* fix: address Sourcery review — strings, enums, hubQuery, StatusColors

• Hardcoded strings → stringResource (Installed, Browse Hub, Statuses, All sources)
• Magic string filters → SkillFilter enum + CATEGORY_ALL constant
• FilterChipRow made generic with chipLabel parameter
• HubBrowseView hubQuery driven from ViewModel state (no dupe)
• ViewModel toasts use getString(R.string.*) via AndroidViewModel
• MaterialTheme.colorScheme.error → LocalHermesStatusColors.current.error

* fix: OpenDrawer → Menu, listContentPadding is PaddingValues not ext

NavIcon.OpenDrawer doesn't exist, use NavIcon.Menu. listContentPadding is a val PaddingValues, not a Modifier extension — use Modifier.padding(listContentPadding).

* fix: NavIcon.Menu(onClick) -> NavIcon.Menu(onOpen)

* fix: pass mockApp to SkillsViewModel in unit tests (now AndroidViewModel)

* fix: mock getString in unit tests for AndroidViewModel string resources

* fix: anyVararg<Any?>() → anyVararg<Any>() to satisfy type bound

anyVararg<T> has bound T : Any, so Any? doesn't compile.
Any is a subtype of Any? at the JVM level, so the matcher
works correctly for vararg Any? parameters.

* fix: use anyVararg<String>() for getString mock to match actual runtime types

The previous anyVararg<Any>() did not match getString(Int, Object...) vararg
calls at runtime — mockk's relaxed mock returned '' for all formatted string
resources, causing all Skills error-path assertions to fail (9 tests).

Root cause: mockk vararg matchers match by reified element type. The Java
method Context.getString(int, Object...) passes format args as String at
runtime (via Kotlin's spread). anyVararg<Any>() with Any (boxed) doesn't
match Object/String vararg elements reliably, but anyVararg<String>() does,
since String satisfies the T : Any bound and matches the actual runtime type.

Also added getString(Int) stub for the no-arg overload used by enums.

* fix: stub getString per-resource-ID to bypass mockk Java vararg bug

mockk has a known bug matching Java vararg methods like
Context.getString(int, Object...) with anyVararg<T>() — the matcher
silently fails to match, and the relaxed mock returns '' for all
formatted string resource calls. This caused 8 Skills error-path tests
to fail (errorMessage/toastMessage was '' instead of containing 'HTTP 500').

Root cause: confirmed via test report artifacts — errorMessage was
non-null but empty string, proving the vararg stub never matched.

Fix: replace the generic anyVararg<String>() stub with explicit per-
resource-ID stubs using eq() + any<String>() matchers. Each formatted
string resource is stubbed individually, returning the format arg
(the dynamic error message) so the test assertions find 'HTTP 500'
and 'Timeout' substrings.

References:
- https://stackoverflow.com/questions/54326914/using-mockk-to-match-any-varargs
- https://github.com/mockk/mockk/issues/224

* fix(skills): replace AndroidViewModel getString() with hardcoded strings to fix unit tests

The SkillsViewModel was using getApplication<Application>().getString() for
error/success messages. In unit tests, mockk cannot reliably stub Java vararg
Context.getString(int, Object...), causing NullPointerException inside
coroutine onError callbacks. All other ViewModels in the codebase use hardcoded
strings; this change aligns SkillsViewModel with that convention.

* test: add hub search/install/uninstall/preview coverage + rm vestigial stubs

- Remove unused mockApp.getString() per-resource-ID stubs (SkillsViewModel
  no longer calls getApplication().getString() — hardcoded strings now).
- Add 10 tests covering hub search (success/empty/error/emptyQuery),
  hub install (success/error), hub uninstall (success/error), and skill
  preview (success/error). Uses existing mockk + runTest patterns.

* fix: correct import ordering for HubSkill (H < S)

Move HubSkill import before KanbanBoard (H < K) and remove duplicate
that was between SessionListResponse and Skill.

* test: remove coVerify from emptyQuery test (cross-test mock state)

* fix: remove double-stacked padding in SkillsScreen (HermesScaffold handles it)

Per AGENTS.md — HermesScaffold already offsets for the top bar
internally, so .padding(padding) on inner content creases double-stacked
top padding.

* refactor: merge preview + edit button in SkillCard into single adaptive action

Per AGENTS.md — two nearly-identical icon buttons (Preview + Edit)
side by side was confusing. Merged into one Edit button since
both opened the same skill content dialog (editable for installed,
read-only for hub is handled by the ViewModel).

* fix: revert onCategoryChange to non-null String (FilterChipRow<T> needs it)

Also removes leftover Preview icon import that ktlint flagged.

* refactor: restructure SkillCard layout — name gets full width, actions below

The old layout crammed icon, name, source badge, edit button, uninstall
button, AND toggle switch all on one horizontal row. Long skill names
got truncated to 2-3 characters.

New layout:
- Top row: icon | name (weight 1f) | source badge | toggle switch
- Below: category (if present)
- Below: description (if present)
- Bottom row: edit + uninstall buttons (end-aligned, only if hub source)

The name Text now has weight(1f) so it fills all available space.

* fix: fix brace nesting + move icon to far left for extra name space

* fix: add missing height import

* refactor: remove Extension icon from SkillCard for cleaner layout

* fix: remove double-stacked padding in SkillEditorDialog and SkillPreviewDialog

Both dialogs had the AGENTS.md footgun: HermesScaffold already offsets
the top bar internally, but the inner Box had .padding(padding) stacking
an extra ~56dp gap between the header and the edit text field.

* fix(skills-hub): unwrap SkillHubSearchResponse wrapper

Backend changed /api/skills/hub/search from returning a raw JSON
array to an object wrapper: { results: [...], source_counts: {...}, ... }

- Added SkillHubSearchResponse, SkillHubInstalledEntry models
- Extended HubSkill with identifier, trustLevel, repo, tags
- Updated API endpoint return type to SkillHubSearchResponse
- ViewModel unwraps data?.results.orEmpty() instead of data.orEmpty()

* fix: unwrap SkillHubSearchResponse in ProfilesViewModel too + remove unused HubSkill import

* fix: update test mocks to use SkillHubSearchResponse wrapper

* fix: refactor HubSkillCard layout — remove icon, name gets full width, install below

* fix: use unique key for hub results + weight(1f) to prevent scroll crash

* fix(skills): resolve crash due to duplicate keys in lazy list

* feat(skills): move search hub button into trailing icon of search bar

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-04 12:54:01 +03:00
M57andHermes Agent d181851670 feat(#441): add profile cloning and description editing (#463)
* feat(#441): add profile cloning and description editing

* feat(#441): add profile builder wizard and API enhancements

Introduce 5-step Profile Builder wizard into ProfilesScreen and add support for create/clone endpoints.

* fix(#441): restore imports and add ProfileInfo constructor defaults

* style: run ktlint format on codebase to fix styling errors

* fix(#441): address PR review comments including provider slug bug, validation, and strings

* fix(#441): remove unused McpServerConfigInput import

---------

Co-authored-by: Hermes Agent <hermes@agent.local>
2026-07-03 22:24:42 +03:00
M57andM57 c0ebc8ec64 feat(#363): add descriptive text to loading spinners (#406)
* feat(#363): add descriptive text to loading spinners

- Updates LoadingState composable to accept optional subtitle
- Shows 'Fetching model providers...' on ModelScreen
- Shows 'Loading channels...' on ChannelsScreen
- Removes dead/unreachable CircularProgressIndicator from ModelScreen

* fix(#363): pass loading_state_subtitle_models string to LoadingState

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-27 01:26:27 +03:00
M57 85c4662537 feat: optimize network stack (#404)
* feat: optimize network stack and connection management

Refactor OkHttp connection pools, consolidate Gson serialization, add network-aware WebSocket reconnects, and deduplicate concurrent requests.

* test: update mapping assertions for AuthExpired

* refactor: address PR review feedback on network stack

Simplify health check, improve authenticator cookie matching robustness, resolve nit imports, and add unit tests.
2026-06-27 00:45:48 +03:00
M57andM57 f190b67ee6 fix(#290): code health — magic colors, flag params, silent catch (#353)
* fix(#290): use LocalHermesStatusColors instead of magic colors (CODE-33)

* fix(#290): replace boolean-flag params with sealed NavIcon class (CODE-37)

* fix(#290): resolve compilation errors — Color import + nullable NavIcon params

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 21:26:32 +03:00
M57andM57 eb71ae3a46 Fix WebSocket events being silently dropped on buffer overflow (#313)
* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix ktlint formatting issues in HermesWsClient.kt

The formatting changes adhere to ktlint rules required by the CI pipeline:
- Removed a trailing comma in MutableSharedFlow initialization to avoid multiline expression wrapping errors.
- Sorted imports to be in lexicographical order.

These formatting changes fix the failing CI check.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow `extraBufferCapacity` from 64 to 256
and changes the `onBufferOverflow` policy to `BufferOverflow.DROP_OLDEST`
in `HermesWsClient.kt`. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting `DROP_OLDEST`, `tryEmit()` remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

* Fix WebSocket events being silently dropped on buffer overflow

Increases the MutableSharedFlow extraBufferCapacity from 64 to 256
and changes the onBufferOverflow policy to BufferOverflow.DROP_OLDEST
in HermesWsClient.kt. This fixes an issue where the slow collection
(e.g., due to DB writes) caused the buffer to fill up and silently drop
incoming real-time WebSocket events.

By setting DROP_OLDEST, tryEmit() remains functional without
suspending, allowing real-time events to be retained and prioritized.

Fixes #275

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 22:13:55 +03:00
M57andM57 2c6e437a5b Fix: Prevent AuthManager state pollution on connection verification (#348)
* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

* Fix: Prevent AuthManager state pollution on connection verification

Modified `ConnectViewModel.connect()` to construct a temporary OkHttpClient and Retrofit builder, isolated from the global `ApiClient` state. The connection verification is executed using this temporary service. Only upon a successful HTTP response does the system persist the configured `host`, `port`, and `token` to the global `AuthManager` and triggers an `ApiClient.rebuild()`. This resolves the issue where a failed connection attempt would overwrite active and verified profiles. Extracted `createTempService` factory to `ApiClient` for robustness and clean tests.

Fixes #260

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-23 20:49:58 +03:00
M57 4d47db263f perf: Add stable keys to Lazy list items for performance (#323)
Added stable `key` parameters to `items(...)` and `itemsIndexed(...)` calls across 13 screens. This prevents Compose from treating all list mutations as full item replacements and eliminates unnecessary full list recompositions during state updates.

Modifications applied to:
- KeysScreen.kt, SharedComponents.kt, AchievementsScreen.kt, PluginsScreen.kt
- SessionsScreen.kt, CronJobsScreen.kt, McpServersScreen.kt, PairingScreen.kt
- ToolsetsScreen.kt, ChatScreen.kt, ModelScreen.kt, SkillsScreen.kt
- KanbanScreen.kt, ProfilesScreen.kt, WebhooksScreen.kt

Note: LogsScreen.kt was left without a key because logs are string lines that can be duplicated, and using `.hashCode()` would cause a "Key was already used" crash. Positional indexing is safer there.

Fixes #279
2026-06-23 12:17:45 +03:00
M57 98c02879b4 Refactor: Extract boilerplate: safeLaunchLoad, clearToast, ToastEffect (CODE-21/22/23) (#312)
- Extracted `safeLaunchLoad` inline extension function to `ViewModelExt.kt` to encapsulate API call success/failure handling logic and replaced usages across 18 viewmodels.
- Extracted `ToastHost` interface to standardize `clearToast()` behavior across all ViewModels displaying toasts.
- Extracted `ToastEffect` composable to `SharedComponents.kt` and applied it to 16 screens, reducing repetitive side effect code blocks.

Fixes #286
2026-06-22 23:54:42 +03:00
M57andM57 60bf6e8a33 feat: UI Changes
* fix(#220): implement edit soul and set model dialogs

* fix(#211): make UI more compact and information dense

* fix(#211): make navigation drawer sidebar more compact

* feat(#252): move settings item to end of navigation drawer

* feat(#250): add bottom navigation bar display mode settings

* feat(#225): add search bar filtering to remaining screens

* fix: make bottom navigation bar display modes more compact

* fix: resolve compilation error in KanbanScreen by referencing assignedTo

* feat: add search to achievements screen and top padding for icon-only navigation bar

* fix: adjust icon-only bottom bar height and remove padding

* fix(#249): remove unused PaddingValues imports in 6 screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-21 20:42:23 +03:00
Hy4ri cdad31b555 refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens 2026-06-21 11:38:33 +03:00
Hy4ri 8efbe02c27 refactor(#78): extract shared components and common strings to resources 2026-06-21 11:00:20 +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
24847cf0d6 refactor(#82): remove dead SkeletonList/SkeletonCard composables
* 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>
2026-06-19 20:45:37 +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 54ebe8714e fix(chat): remove double padding and zero out nested Scaffold window insets 2026-06-19 18:37:48 +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
M57andM57 4892572bcc feat(#13): add pull-to-refresh to HermesScaffold (#14)
- 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>
2026-06-18 18:58:08 +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
M57 d90e36974c feat: Material You design system rewrite — Phases 1–4
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.
2026-06-18 16:34:44 +03:00
7daf73ce0d feat(ui): modern theme, sectioned drawer, shared components (v1) (#1)
* 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>
2026-06-18 02:59:43 +03:00