Commit Graph
38 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
Hy4ri 5ecc2e7d88 refactor(skills): decompose SkillsScreen into focused UI components
Extract InstalledSkillsView, HubBrowseView, SkillCard, HubSkillCard,
SourceChip, SkillScanViews, and SkillPreviewDialog into components/.
2026-09-11 18:44:09 +03:00
Hy4ri 9eaf9663f3 refactor(skills): extract SkillListFilter and SkillScanPresentation
Separate pure skill filtering and scan presentation helpers.
Wire into SkillsScreen with unit test coverage.
2026-09-11 17:58:41 +03:00
Hy4ri 4287723ab0 refactor(skills): reconcile duplicate components and drop dead category filter
Use extracted components/SkillEditorDialog and SourceBadge.
Drop unused CategoryFilter enum.
2026-09-11 17:45:33 +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 90c1e8dc5f fix: tab reloads actually reload — kanban switch spam, plugins/skills refresh, ws mint (#897)
- ws: ticket-mint response body was read on the calling (main) thread —
  the socket read threw NetworkOnMainThreadException, the mint failed,
  and the kanban events stream never connected (board stuck Offline).
  execute() + body read now run entirely on Dispatchers.IO; same fix in
  DashboardSessionTokenRefresher.fetch (loopback mint path).
- kanban: re-selecting the already-active board reloads data and
  reconnects events WITHOUT the redundant POST /boards/{id}/switch that
  refresh fired on every tap.
- plugins: top-bar Refresh + pull-to-refresh wired through HermesScaffold
  (previously no reload control at all).
- skills: Refresh icon now does a plain GET reload; the heavy hub-update
  POST moved to the cloud icon so refresh is no longer disguised as update.
- test: mock the 3-arg Log.w overload (catch now logs the full stack).

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 14:27:36 +03:00
M57andM57 e639e33188 chore(skills): delete dead InstalledSkillsTab.kt (issue #797) (#816)
Orphaned duplicate of SkillsScreen's private InstalledSkillsView. Zero
references in the repo (no imports, call sites, or tests). Shared helpers
(CATEGORY_ALL, SkillFilter, SkillsUiState) live in SkillsScreen.kt /
SkillsViewModel.kt, not this file. No behavior change.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-04 20:09:01 +03:00
M57andM57 1870b375b1 feat(skills): hub sources + featured landing and security scan (#779) (#793)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-03 22:13:28 +03:00
M57andM57 cf18c3a02e feat(skills): redesign view mode switcher and fix status filter alignment & card padding (#704)
* feat(skills): redesign tab switcher and fix status filter alignment and card padding

* fix(skills): replace status filter chips with SingleChoiceSegmentedButtonRow for precise alignment

* fix(skills): left-align status, category, and source filter chip rows

* fix(skills): explicitly left-align status FilterChips with 16dp start margin

* fix(skills): match status filter chip row styling and All label to category row below it

* fix(skills): remove extra Row wrapper padding from status filter chips in SkillsScreen

* feat(skills): enhance card background contrast and reduce side padding for Installed and Hub cards

* feat(skills): reduce card/search padding to 8dp and pin top controls with weight(1f) on LazyColumn

* style(skills): fix ktlint import-ordering and remove unused imports (PR #704)

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-24 01:10:12 +03:00
M57andM57 2e97ee9a84 refactor: remove verified dead code across data, UI, and resources (#680)
* refactor: remove unused dead code across data, UI, and resources

* fix: resolve dangling references left by dead-code removal

Restore empty object Motion (braces required) and remove the 12 stale
test usages that still called the now-deleted production symbols:
- ServerStoreTest: removeServer/switchToServer tests
- ServerEndpointTest: isCleartext assertions
- ConnectViewModelTest: onHostChange/onPortChange tests + 3 dropped R.string refs
- SettingsViewModelTest: renameProfile tests + clearNavigateToLogin
- E2eIntegrationTest: previewSkill tests + onHostChange/onPortChange calls

Verified locally: compileDebugUnitTestKotlin + 48 tests pass.

* fix: remove unused imports flagged by ktlint

Drops 4 no-longer-referenced imports left by the dead-code removal:
- HermesApiService: SkillScanResponse (scanHubSkill removed)
- ServerEndpointTest: assertFalse, assertTrue (isCleartext assertions dropped)
- SettingsViewModelTest: assertFalse (rename tests removed)

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-22 15:51:07 +03:00
6a421d7a84 feat(ux): add CTAs to EmptyState across screens (issue #618) (#627)
Turn dead-end empty states into on-ramps by wiring the existing
EmptyState actionLabel/onAction params to real next actions.

Screens wired (real, existing actions):
- Skills (installed): Browse skill hub -> setViewMode(HUB)
- Skills (hub): Retry search -> searchHub(hubQuery)
- Cron Jobs: Create your first job -> openNewJobDialog()
- MCP servers: Add a server -> toggleAddForm() (literal moved to strings.xml)
- Webhooks: Create subscription -> showCreateDialog() (literal moved to strings.xml)
- Channels: Connect a Telegram channel -> startTelegramOnboarding()
- Model: Configure model -> navigateTo(SettingsConnection)
- Sessions (history): Start a new chat -> navigateTo(ChatScreen)
- Analytics: Explore Skills or Cron jobs -> navigateTo(SkillsScreen)
- Keys (no match): Clear search -> query = ""

All new labels use strings.xml (empty_action_* / screenname_empty_*).

Deviations from issue (documented honestly):
- Toolsets, Profiles, Plugins keep Refresh. No in-app create path exists
  (toolsets are server-defined toggles; profiles/plugins have no create
  dialog wired in UI). Refresh is the correct recovery there, not a fake
  'create' CTA. Flagged for follow-up if create flows get built.
- Skills: edited the real private InstalledSkillsView in SkillsScreen.kt;
  the duplicate internal one in InstalledSkillsTab.kt is dead code.

Verified: ktlintCheck clean, assembleDebug green.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Ronia <ronia@hermes>
2026-07-18 16:42:32 +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
M57 d1fc477bcd fix: resolve all build compilation and style check warnings (#597)
* fix: resolve all build compilation and style check warnings

* fix: remove unused import in SystemViewModel
2026-07-14 21:55:37 +03:00
M57 480d29e481 fix(#545): fix gated ws auth loop and ticket refresh (#546)
Fix gated close codes, run ticket refresh on Dispatchers.IO, prevent scope race on startup, and auto-format files.
2026-07-11 00:21:23 +03:00
M57 793ab89c14 fix: move skill source badge beside install button in hub cards (#502)
The hub card UI is rendered by the local private HubSkillCard in SkillsScreen.kt (not the unused components/HubSkillsTab.kt copy). Moved SourceBadge out of the top-right name row to the left of the Install button in the card footer (left-aligned source, right-aligned action), tying the skill origin (hub/clawhub/etc) to the install action.
2026-07-08 23:09:19 +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
M57 ca32e37e2f refactor(ui): split monolithic screen files into focused components (#485)
* refactor(ui): extract SkillsStep to components package

Move the 252-LOC SkillsStep composable out of ProfilesScreen.kt into
ui/profiles/components/SkillsStep.kt. Body is verbatim (behavior-preserving);
only package + imports changed. Parent now imports and calls it directly.

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).
Note: sibling screens resolve Modifier.weight() without an explicit import;
matching that convention (no spurious weight import) to avoid the
internal RowColumnParentData.weight shadowing error.

* refactor(ui): extract ProfileBuilderView cluster to components package

Move the Profile Builder wizard (ProfileBuilderView + validateProfileName +
IdentityStep/ModelStep/McpStep/AddMcpDialog/ReviewStep) out of ProfilesScreen.kt
into ui/profiles/components/ProfileBuilderView.kt. Body is verbatim
(behavior-preserving); only package + imports changed.

Notable wiring required:
- validateProfileName kept as internal fun: it is also used by the parent's
  clone dialog, so both the screen and the builder share it via import.
- ProfilesUiState/ProfilesViewModel imported from the ui.profiles package.
- SkillsStep call resolves to the already-extracted sibling file (same package).

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).

* refactor(ui): extract MoaConfigDialog cluster to components package

Move the Mixture-of-Agents config dialog (MoaConfigDialog + MoaConfigEditor +
MoaSlotPicker + MoaPickerTarget sealed class) out of ModelScreen.kt into
ui/model/components/MoaConfigDialog.kt. Body is verbatim (behavior-preserving);
only package + imports + entry visibility changed.

- Entry MoaConfigDialog is internal (called by ModelScreen root); the editor,
  slot picker, and sealed class stay private to the new file.
- The @Composable annotation preceding the declaration is moved with the
  cluster (earlier attempt left it orphaned, causing a duplicate-annotation
  compile error at the next declaration).

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).

* refactor(ui): extract PlatformCard + TelegramOnboardingDialog clusters

Move two self-contained clusters out of ChannelsScreen.kt:
- PlatformCard.kt: StateStyle (private data class) + platformStateStyle +
  StateBadge + PlatformCard. Entry PlatformCard is internal (called by
  ChannelsScreen root); StateStyle/StateBadge/platformStateStyle stay private.
- TelegramOnboardingDialog.kt: TelegramOnboardingDialog. Entry internal.

Wiring required:
- ConfigureForm kept in ChannelsScreen as internal (shared by PlatformCard
  and the screen root) and imported into PlatformCard.kt.
- ChannelsUiState/OnboardingPhase imported from the ui.channels package.
- formatTimestamp left in the screen (used by ConfigureForm).

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).

* refactor(ui): extract SkillsScreen sub-composables to components package

Split the 914-line SkillsScreen.kt into the screen root plus 4 cohesive
component files under ui/skills/components/:
- SourceBadge.kt: shared SourceBadge (internal, used by both tabs)
- InstalledSkillsTab.kt: InstalledSkillsView + SkillCard + SkillPreviewDialog
- HubSkillsTab.kt: HubBrowseView + HubSkillCard
- SkillEditorDialog.kt: SkillEditorDialog

Wiring required:
- CATEGORY_ALL promoted private -> internal in SkillsScreen (shared by the
  screen root and InstalledSkillsView) and imported where needed.
- SkillFilter / SkillsUiState imported from the ui.skills package.
- HubBrowseView now reads isHubSearching/hubResults/hubSearchError off its
  SkillsUiState param (fields, not locals) - no behavior change.

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).

* refactor(ui): extract SettingsScreen + SystemScreen sub-composables

SettingsScreen.kt (1238 -> ~340): move self-contained sections into
ui/settings/components/ - ConnectionSection (+TestResultCard/SaveIndicator/
TestConnectionButton), BehaviorSection, AppearanceSection (+ChatSection/
NavBarSection), AboutSection, ProfileEditorDialog (+DeleteProfileConfirmDialog).
Shared leaves SectionCard/InfoRow kept in the screen as internal and imported
by the extracted files.

SystemScreen.kt (1755 -> 1619): move CredentialEntryRow + HookCard into
ui/system/components/ (both internal). formatBytes/formatDuration helpers stay
in the screen (used by the root and remaining content).

Verified: ktlintCheck PASS, compileDebugKotlin PASS (local SDK).

* fix(#485): use LocalHermesStatusColors for PlatformCard state colors

Map platform connection-status badges to semantic LocalHermesStatusColors
slots (success/warning/error) + add neutral slot for not_configured/disabled.
Removes hardcoded hex (Color(0xFF...)) and MaterialTheme.colorScheme usage
flagged by Sourcery; neutral defaults to StatusGrey so existing theme
providers are untouched.
2026-07-07 23:00:24 +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
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
Hy4ri cdad31b555 refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens 2026-06-21 11:38:33 +03:00
Hy4ri 19d44c077b chore: format codebase with ktlint 2026-06-21 10:57:18 +03:00
M57 63575272c9 feat(skills): add category and status filters (#111) (#199) 2026-06-21 00:49:58 +03:00
M57andronia 113467a99d fix(#140): fix skills content API endpoints — path params to query params (#142)
Co-authored-by: ronia <ronia@m57>
2026-06-20 02:43:26 +03:00
M57 46b24593f9 feat(#58): implement skill markdown/yaml editor UI (#121) 2026-06-19 23:58:34 +03:00
Hy4ri 508aea8bfa fix(#79): centralize retrofit error handling with NetworkResult 2026-06-19 22:58:33 +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
Hy4ri 0562906634 fix: replace refresh text button with refresh icon across all screens (Kanban, Cron Jobs, Skills, Channels, Plugins, System) 2026-06-16 00:41:27 +03:00
Hy4ri d6ff3de594 Initial commit of HermesControl Android app 2026-06-16 00:00:36 +03:00