* feat(kanban): live board updates over plugin events WebSocket (#775)
Tail /api/plugins/kanban/events per board with cursor tracking and
backoff reconnect; debounced REST reload on event batches (desktop's
proven pattern — event payloads are partial updates). Board switch
re-subscribes with a fresh stream. Reuses HermesWsClient's ticket
refresh (now internal). Adds ServerEndpoint.webSocketUrl(path, ...)
overload for non-chat WS endpoints.
Kanban UI pass: add-task FAB (the AddTaskDialog was unreachable),
live/offline status pill, assignee chip on task cards, 2-line
description clamp, empty-column hint.
Tests: KanbanEventsClient frame parsing, KanbanViewModel WS wiring
(connect on load, debounced reload, board-switch reconnect,
no-reconnect guard, onCleared), ServerEndpoint overload.
* fix(kanban): create task response is wrapped {"task": ...} — read it as Unit
Backend POST /api/plugins/kanban/tasks returns {"task": {...}} (wrapped),
but the app declared Response<KanbanTask> — with ignoreUnknownKeys=true
the wrapper key is skipped and the required id/title/status fields are
missing, so deserialization threw and the create showed a failure toast
while the task WAS created server-side (ghost tasks). The body is never
consumed by the ViewModel, so Response<Unit> matches updateKanbanTask.
FAB from the live-events PR (#775) made this latent bug reachable.
Adds an E2e regression assertion for the create path.
* feat(kanban): remove move left/right arrows — backend rejects the transitions
The dashboard PATCH route's status whitelist omits 'review' (a first-class
kanban_db status), so any move touching that column 400s, and reopen
transitions (done->blocked) 409. Every task on the live board sits in
'done', making the arrows 100% failure. Removing them keeps the UI honest;
tasks are managed from the desktop/CLI.
Drops moveTask/revertTaskMove from the VM, the arrows from TaskCard, the
now-dead updateKanbanTask API method and move strings; E2e test keeps
load/create coverage.
* feat(kanban): desktop-style task transitions — gated action sheet
Mirrors the desktop kanban's move model: tap a card to open an action
sheet with only the transitions the backend PATCH route accepts:
- triage / ready from any status (direct write; unblock from blocked)
- block only from running/ready, complete only from running/ready/blocked
- archive from any non-archived status
- never running (dispatcher-only, backend 400s) and never review (not in
the dashboard whitelist) — the moves that made the old arrows lie
Complete prompts for a mandatory completion summary (stored as result),
matching the desktop; block/complete/archive ask for confirmation.
Optimistic update with revert + toast on failure.
Tests: transition-gating map, move PATCH body, failure revert, completion
summary body; E2e kanban test covers a desktop-style todo->ready move.
* ui(kanban): replace oversized FAB with compact top-bar add button
The 56dp FAB floated over the board's bottom-right corner and covered a
chunk of the last column. The add action now lives in the header row next
to the live pill (input-top style), and the board's bottom contentPadding
drops back to 16dp — no more reserved dead zone.
* fix(kanban): mint an independent WS ticket — don't touch the chat's shared slot
The dashboard WS ticket is single-use and consumed at handshake. Both the
chat client and the kanban events client were refreshing into the SAME
shared AuthManager token slot on their own schedules (the kanban client on
every backoff reconnect, forever). Whichever client built its URL second
grabbed the other's just-consumed ticket, got 1008, reconnected, and stole
the other's next ticket — mutual starvation that left the app stuck at
'Reconnecting...' (repro on main: none, because main has one consumer).
HermesWsClient now exposes mintWsTicket(): a ticket minted WITHOUT touching
the shared slot (cookie-auth'd, same endpoint). The kanban events client
uses its own ticket per connection; the chat WS keeps the shared slot.
Refresh's HTTP-error -> status mapping is preserved via a small result
holder.
* fix(kanban): drop orphaned ko translations for removed move buttons
The move arrows were removed earlier but values-ko still shipped
content_desc_move_left/right — Android lint ExtraTranslation failed CI.
* fix(ws): keep RECONNECTING on transient ticket-mint exceptions
My refactor flattened the ticket-mint failure mapping: a network exception
during POST /api/auth/ws-ticket was reported as a null HTTP code and
mapped to DISCONNECTED, breaking the #740 contract that transient failures
stay in RECONNECTING (and with it testGatedMode_transientTicketFailure-
KeepsReconnectFlow). TicketRequestResult now carries an exception flag and
refreshWsTicketIfNeeded maps it to RECONNECTING.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* [feature] issue #634: HTTPS + reverse-proxy server endpoint support
- Add ServerEndpoint model (scheme-aware base URL, resolve/webSocketUrl/relativeRequestPath, cleartext policy)
- AuthManager: baseUrl is canonical; setHost/setPort derive from endpoint; endpointForBuild() for scheme-aware URLs
- ApiClient/CookieManager/HermesWsClient: route through ServerEndpoint (no hardcoded http://)
- Data migration: ServerUrlMigration converts legacy host/port to baseUrl
- UI: Connect/AuthLogin/Settings now use a single Server URL field with transport warning
- Build: ALLOW_CLEARTEXT buildConfigField + manifest placeholder for dev
- Tests: ServerEndpointTest + ServerUrlMigrationTest; adapt existing suite to baseUrl API
Co-Authored-By: M57 <Hy4ri@users.noreply.github.com>
* fix(authlogin): basic-auth derives token automatically, no manual entry
connectBasicAuth() built a fresh OkHttpClient for the password-login call and
a different one for the ws-ticket call, so the Set-Cookie from
/auth/password-login (the session token lives in the cookie, not the JSON body)
never reached /api/auth/ws-ticket. That 401'd the ticket request, returned null,
and the UI fell back to asking the user to paste a token.
Both calls now use the shared OkHttpProvider.probe client (same CookieManager
jar), so the session cookie flows through and the WS ticket mints automatically —
matching the dashboard contract (routes.py: password-login sets Set-Cookie,
ws-ticket requires it).
Add AuthLoginBasicAuthFlowTest proving the cookie propagates on the shared
client (and that ws-ticket 401s without it).
Fixes the basic-auth token prompt reported against issue #634.
* chore: remove TASK-634-TESTS.md (stray task-tracking doc, not part of feature)
* ci: broaden .gitguardian.yml test-file ignores (unit + instrumented + variant dirs)
* feat(authlogin): smart auth-mode detection from /api/status + drop pairing-string login
- Replace 302-sniff probe with authoritative GET /api/status read
(auth_required + auth_providers) to derive DashboardAuthMode.
- Add OAUTH mode (stubbed, 'coming soon' UI, TODO tracks issue #639).
- Render login UI by detected mode: token (loopback), username+password
(basic/gated), or OAuth notice.
- Remove legacy pairing-string box + onPairingString/applyLegacyPairing.
- Extract internal deriveAuthMode() + add 5 unit tests.
Closes nothing; OAuth tracked in #639.
* fix(chat): don't block WS connect on empty token in gated (ticket) mode
connectWebSocket() gated on getToken() != null, correct for loopback
(token) mode but wrong for gated (ticket) mode: the ticket is minted
fresh by HermesWsClient.refreshWsTicketIfNeeded() from the persisted
session cookie, so getToken() is expected empty at connect time. The
early return left the WS unopened -> DISCONNECTED -> chat showed
'reconnect' immediately after basic-auth login.
Only require a non-blank token in non-gated mode.
Fixes#640.
* test: remove onPairingString test cases for deleted pairing-login path
The pairing-string login (onPairingString/applyLegacyPairing) was removed
from ConnectViewModel, so the 5 tests that exercised it (3 in
ConnectViewModelTest, 2 in E2eIntegrationTest) no longer compile. Remove
them. Also drop the now-orphaned connect_pairing_string from values-ko.
Fixes the Unit & Integration Tests CI failure on PR #638.
---------
Co-authored-by: hy4ri <hy4ri@users.noreply.github.com>
Backend commit 37a4cf900 (hermes-agent) flipped the default of
GET /api/model/options's include_unconfigured parameter from true
to false, so the mobile Models picker was silently regressing to
showing only configured/authenticated providers — the skeleton rows
for unconfigured providers (with setup-hint warnings) stopped arriving.
Add include_unconfigured as a @Query param defaulting to true so the
full provider universe is restored on the Models screen. Both current
call sites (ModelViewModel + ProfilesViewModel) invoke the API without
passing the new param, so they automatically pick up the true default.
* fix(#478): rework default login to use unified ConnectionProfile model
- AuthManager: drop legacy auth_token path; getToken/setToken now delegate
to the selected profile (profile token only, no global fallback)
- setHost/setPort always write the selected profile (no top-level host/port branch)
- Add DEFAULT_PROFILE_ID + ensureDefaultProfile()/ensureDefaultSelected() so the
selected profile is never null; ensureDefaultSelected() only falls back when
nothing is selected (never clobbers an existing choice); ensureDefaultProfile()
only injects a Default profile on fresh install, never into an existing list
- One-time migration folds legacy standalone auth_token into the Default profile,
batched into a single SharedPreferences transaction (guarded by legacy_default_migrated)
- ConnectViewModel.connect() persists via setToken/setHost/setPort (delegate to
the selected profile) — keeps the existing persistence contract
- Connect/Settings: remove the '(none)'/standalone profile option; deleteProfile
falls back to Default profile instead of null selection
- Update AuthManagerTest/ConnectViewModelTest to the unified model; fix test
stub ordering + migration re-init; remove dead connect_profile_none / settings_profile_none strings
Closes#478
* fix: stub AuthManager.ensureDefaultSelected() in E2eIntegrationTest
The #478 ConnectViewModel.connect() success branch now calls
AuthManager.ensureDefaultSelected() on the non-saveProfile path.
E2eIntegrationTest.setUp() mocks AuthManager as a non-relaxed mockkObject
but never stubs that method, so the unstubbed call throws MockKException inside
viewModelScope.launch, leaving connectionSuccess unset and failing 3 tests:
- testFullUserSessionFlow
- testConnectViewModel_onPairingString_base64Format
- testConnectViewModel_onPairingString_urlFormat
Add the missing stub. Verified locally: full suite 322/322 passing.
---------
Co-authored-by: ronia <ronia@m57>
* feat(#480): pass refresh=true on Models pull-to-refresh to re-probe custom providers
* test(#480): assert refresh flag propagation for default vs pull-to-refresh loads
* fix(#480): capture refresh arg via slot instead of brittle coVerify in propagation test
---------
Co-authored-by: ronia <ronia@m57>
* refactor: migrate Gson serialization to kotlinx.serialization
* fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement
* test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults
* fix(chat): support flexible numeric and string decoding for message timestamps
* fix(chat): sequentialize cache loading to avoid overwriting network state
* 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>
* feat(#442): categorized env keys screen with add/remove, search, and gateway restart
- Group env vars by category (LLM Providers, Tool API Keys, Messaging
Platforms, Agent Settings, Other) with collapsible AnimatedVisibility
sections matching MCP screen pattern
- Add new env var form (PUT /api/env) with key/value inputs
- Delete env var via DELETE /api/env with EnvVarDeleteRequest body
- Reveal/hide/edit inline per-card controls
- Search bar filtering across key names and descriptions
- Gateway restart banner with one-click action when changes are dirty
- Track keysChanged flag showing restart prompt after mutations
- Wire delete endpoint in HermesApiService using @HTTP(hasBody=true)
- Add EnvVarDeleteRequest data model class
* fix(#442): update test to match new categorized KeysViewModel API
* fix(#442): satisfy ktlint multiline-expression-wrapping rule
---------
Co-authored-by: Hermes Agent <hermes@agent.local>
* feat(#434): add system tab model files
* feat(#434): add system API endpoints
* feat(#434): rewrite SystemViewModel with full parity
* feat(#434): add system screen UI and string resources
* feat(#434): update tests for new SystemStatsResponse & ViewModel
* fix(#434): fix compile errors — nullable Boolean, SmallFilledTonalButton -> FilledTonalButton
* fix(#434): fix old SystemStatsResponse refs in ChatViewModel, wrap sections in item{} blocks
* fix(#434): rename sections to lowercase, fix LazyListScope extensions
* fix(#434): wrap hook dialog in item{}, pass spacing param to actionLogSection
* fix(#434): update HermesApiServiceTest for new typed SystemStatsResponse
* fix(#434): fix E2E test coroutine timing, remove outdated Gson edge-case test
* fix(#434): fix JUnit 4 assertNotNull argument order
* fix(#434): simplify HermesApiServiceTest, restore all mocks in E2e test
* fix(#434): remove Gson-edge-case test, fix E2E test dispatcher wiring
- Removed testGetSystemStats_nonNumericPercent_returnsNull: tests Gson
type-coercion behavior, not app logic — throws JsonSyntaxException
- Fixed E2eIntegrationTest dispatcher: mock Dispatchers.IO with answers {}
that returns the current Dispatchers.Main dynamically, so runTest's
per-test dispatcher override is honored by both viewModelScope.launch
and async(Dispatchers.IO) blocks → advanceUntilIdle() advances all work
- Removed Dispatchers.Main static mock so runTest can override it
* fix(#434): fix E2E test dispatcher for SystemViewModel
- Removed Dispatchers.setMain + testDispatcher field from setUp
- runTest handles Dispatchers.Main automatically — don't override it
- Mock Dispatchers.IO with answers { Dispatchers.Main } so it
dynamically resolves to whatever Main is at call time (runTest's
TestScope dispatcher)
- This ensures viewModelScope.launch and async(Dispatchers.IO) both
run on the same TestScope dispatcher, making advanceUntilIdle()
effective for all work
* fix(#434): use runTest(testDispatcher) for SystemViewModel test
The SystemViewModel.loadAll() uses async(Dispatchers.IO) inside
coroutineScope { }. With the mocked Dispatchers setup, both Main and
IO return testDispatcher, but runTest()'s default TestScope uses a
different dispatcher instance whose advanceUntilIdle() can't advance
testDispatcher's scheduler.
Fix: pass testDispatcher to runTest() so the TestScope's scheduler
matches testDispatcher's scheduler, making advanceUntilIdle()
effective for all async work.
* fix(#434): align test assertion with toast message change
The SystemViewModel.triggerBackup() toast message was changed from
'Backup triggered successfully' to 'Backup triggered', but the
E2eIntegrationTest assertion wasn't updated, causing a
ComparisonFailure on the unit-tests CI job.
* fix(#434): address review issues
- Fix#1: Change triggerBackup() return type from Unit to ActionResponse
so backupArchive is captured, enabling the download button
- Fix#2: Replace deprecated LocalClipboardManager with LocalClipboard
suspend API (removes CI deprecation warning)
- Fix#3: Consolidate redundant 'Hermes version' InfoRow in gateway
section (sessions + PID shown in one row)
- Remove orphaned system_gateway_state_pid string resource
- Fix import ordering for ktlint compliance
* fix(#434): revert LocalClipboard change, use @Suppress('DEPRECATION') instead
LocalClipboard.setText is not available in the Compose version used
by the project. Reverting to LocalClipboardManager with a suppress
annotation to keep the deprecation warning clean without breaking
compilation.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@agent.local>
* feat(#445): full Models tab — settings panel, aux tasks, MOA config, model assignment
Main features added:
- Model Settings card at top of screen with main model, aux tasks, MOA summary
- Main model assignment via /api/model/set with expensive model confirm flow
- Auxiliary tasks dialog with per-task model override and reset-all
- MOA config dialog with preset management and reference/aggregator model picker
- Model picker dialog reused for main model, aux tasks, and MOA slot selection
- Kept existing provider browse + pin/unpin/search
New data models: AuxiliaryModelsResponse, MoaConfigResponse,
ModelAssignmentRequest/Response
New API endpoints: GET /api/model/auxiliary, GET/PUT /api/model/moa,
POST /api/model/set
* test(#445): add aux + MOA mock endpoints to E2eIntegrationTest
testModelOptionsSelection_success was failing because the ViewModel's init
block now calls getAuxiliaryModels() and getMoaConfig() which weren't mocked.
Added proper mock returns + imports for the new types.
* fix(#445): correct mock endpoint name in E2eIntegrationTest
- getMoaConfig -> getMoaModels (matches API service method name)
- Add explicit type parameter Response.success<MoaConfigResponse>
to fix Kotlin type inference
* fix(#445): add missing main param to AuxiliaryModelsResponse mock
AuxiliaryModelsResponse requires a MainModelAssignment for its
'main' field. Added the parameter with a dummy value and the
corresponding import.
* fix(#445): update toast assertion in test to match new message format
ViewModel now produces 'Successfully set profile model to ronia'
instead of the old 'Successfully set model to ronia'.
* fix(#445): remove top padding from Models tab LazyColumn
Top inset already provided by HermesScaffold; double padding was chunky next to TopAppBar.
* fix(#445): remove double-stacked top padding on Models tab LazyColumn
HermesScaffold's Box already applies padding(paddingValues) to
offset content below the TopAppBar. The LazyColumn was re-applying
it on the modifier, doubling the top gap. Keep the modifier clean
(.fillMaxSize() only) — the Box handles the offset.
* refactor(#445): inline loadModelOptions into loadAll
loadAll() was just delegating to loadModelOptions() with no extra
logic. Consolidate into a single method to remove the redundancy.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(#445): full Models tab — settings panel, aux tasks, MOA config, model assignment
Main features added:
- Model Settings card at top of screen with main model, aux tasks, MOA summary
- Main model assignment via /api/model/set with expensive model confirm flow
- Auxiliary tasks dialog with per-task model override and reset-all
- MOA config dialog with preset management and reference/aggregator model picker
- Model picker dialog reused for main model, aux tasks, and MOA slot selection
- Kept existing provider browse + pin/unpin/search
New data models: AuxiliaryModelsResponse, MoaConfigResponse,
ModelAssignmentRequest/Response
New API endpoints: GET /api/model/auxiliary, GET/PUT /api/model/moa,
POST /api/model/set
* test(#445): add aux + MOA mock endpoints to E2eIntegrationTest
testModelOptionsSelection_success was failing because the ViewModel's init
block now calls getAuxiliaryModels() and getMoaConfig() which weren't mocked.
Added proper mock returns + imports for the new types.
* fix(#445): correct mock endpoint name in E2eIntegrationTest
- getMoaConfig -> getMoaModels (matches API service method name)
- Add explicit type parameter Response.success<MoaConfigResponse>
to fix Kotlin type inference
* fix(#445): add missing main param to AuxiliaryModelsResponse mock
AuxiliaryModelsResponse requires a MainModelAssignment for its
'main' field. Added the parameter with a dummy value and the
corresponding import.
* fix(#445): update toast assertion in test to match new message format
ViewModel now produces 'Successfully set profile model to ronia'
instead of the old 'Successfully set model to ronia'.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(#369): rewrite channels tab with full backend integration
- Add state, error, gateway_running, description to model
- Add test endpoint to API service
- Rewrite ViewModel with toggle, restart, test actions
- Rewrite screen with state badges, restart banner, test button
- Fix string resources and E2E test
* fix(#417): add missing import and fix ButtonSize API
- Add MessagingPlatformTestResult import to HermesApiService.kt
- Replace ButtonDefaults.ButtonSize.SMALL with ButtonDefaults.SmallButtonSize (correct M3 API)
* fix(#417): mark platformStateStyle as @Composable, remove Button size param
- @Composable annotation needed for MaterialTheme.colorScheme access
- Button/OutlinedButton size parameter not available in project's M3 version
- ktlint removed unused ButtonDefaults import
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(#365): add pin support for models with pinned section at top
* fix(#365): stub Pinned Models methods in E2eIntegrationTest
* fix(#365): use state flow for pin/unpin, add max cap, add ViewModel tests
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix(#219): add pagination to sessions history tab
The /api/sessions endpoint was called without pagination params, so the server returned only its default limit of 20 sessions.
- SessionListResponse: added total/limit/offset fields from server response
- HermesApiService: added @Query params (limit, offset, order) to getSessions()
- SessionsViewModel: added loadMore() that appends pages, hasMore derived from total
- SessionsScreen: added 'Load more sessions' button + loading spinner at bottom of list
* chore: remove plan file from git
* fix: compilation errors in sessions pagination
* chore: gitignore plan files
* fix: remove duplicate brace
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* Fix Medium severity performance findings (PERF-17 to PERF-21)
This commit addresses the following performance findings:
- PERF-17: Implemented a message queue in `HermesWsClient` using `ConcurrentLinkedQueue`. If the WebSocket is disconnected, messages are queued and flushed upon a successful connection (`onOpen`).
- PERF-18: Added exponential backoff to `safeApiCall` for 5xx and IO exceptions, reducing the load on a struggling server.
- PERF-19: Optimized Kanban task filtering by performing `groupBy` upfront inside a `remember` block and passing item keys to `LazyColumn`, avoiding O(C×T) calculation per recomposition.
- PERF-20: Updated `getLogs` API in `HermesApiService` to accept `lines` and `limit` query params to truncate logs early, avoiding potential JSON parsing OOMs with large log files.
- PERF-21: Optimized drawer entry generation in `Navigation.kt` by grouping navigation items inside a `remember` block.
Fixes#282
* Fix Medium severity performance findings (PERF-17 to PERF-21)
This commit addresses the following performance findings:
- PERF-17: Implemented a message queue in `HermesWsClient` using `ConcurrentLinkedQueue`. If the WebSocket is disconnected, messages are queued and flushed upon a successful connection (`onOpen`).
- PERF-18: Added exponential backoff to `safeApiCall` for 5xx and IO exceptions, reducing the load on a struggling server.
- PERF-19: Optimized Kanban task filtering by performing `groupBy` upfront inside a `remember` block and passing item keys to `LazyColumn`, avoiding O(C×T) calculation per recomposition.
- PERF-20: Updated `getLogs` API in `HermesApiService` to accept `lines` and `limit` query params to truncate logs early, avoiding potential JSON parsing OOMs with large log files.
- PERF-21: Optimized drawer entry generation in `Navigation.kt` by grouping navigation items inside a `remember` block.
Fixes#282
* Fix Medium severity performance findings (PERF-17 to PERF-21)
This commit addresses the following performance findings:
- PERF-17: Implemented a message queue in `HermesWsClient` using `ConcurrentLinkedQueue`. If the WebSocket is disconnected, messages are queued and flushed upon a successful connection (`onOpen`).
- PERF-18: Added exponential backoff to `safeApiCall` for 5xx and IO exceptions, reducing the load on a struggling server.
- PERF-19: Optimized Kanban task filtering by performing `groupBy` upfront inside a `remember` block and passing item keys to `LazyColumn`, avoiding O(C×T) calculation per recomposition.
- PERF-20: Updated `getLogs` API in `HermesApiService` to accept `lines` and `limit` query params to truncate logs early, avoiding potential JSON parsing OOMs with large log files.
- PERF-21: Optimized drawer entry generation in `Navigation.kt` by grouping navigation items inside a `remember` block.
Fixes#282
* fix: resolve ktlint formatting issues in KanbanScreen.kt and Navigation.kt
* fix: fix ktlint import ordering in HermesWsClient.kt
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* 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>
* fix(#298): internationalize ConnectViewModel strings, fix null-assert crash on profile selector
E1: Add all connect error resources to strings.xml for i18n readiness
E2: Migrate ConnectViewModel to AndroidViewModel, replace all hardcoded
user-facing strings with getString(R.string.*) calls
F1: Replace state.selectedProfile!! with .?.let { } pattern in
ConnectScreen.kt to eliminate NPE risk on mutable state copy
Closes#298
* fix(#298): update test files for AndroidViewModel constructor
Update ConnectViewModelTest.kt and E2eIntegrationTest.kt to pass
mock Application to the new ConnectViewModel(app) constructor.
Add getString() MockK stubs matching the string resource keys.
* fix(#298): add missing LocalContext import and restore connect_pairing_string
Two compilation errors:
- ConnectScreen.kt was missing import for LocalContext
- strings.xml was missing connect_pairing_string resource
(accidentally removed in earlier patch)
* fix(#298): extract ViewModelFactory to avoid composable-in-non-composable error
LocalContext.current cannot be called inside a viewModel { } factory
lambda because the lambda is not @Composable. Fix by extracting a
ConnectViewModelFactory class and using run { } to capture the
Application value at the composable call site before passing to
the factory constructor.
* fix(#298): fix MockK varargs matching in test stubs
getString(int, Object...) takes varargs. MockK's any<Int>() and
any<String>() didn't match the vararg Object... slot. Changed to
any() (untyped) which matches correctly.
* fix(#298): fix MockK answers — arg indexing was wrong
In getString(int, Object...), firstArg is the resource ID,
secondArg is the format argument. Using firstArg<Int>() in
the answers block was returning the res ID, not 500/etc.
Changed to secondArg<Any>().
* fix(#298): replace getString(int,Object...) with String.format(getString(int),...)
Avoid MockK varargs matching issues by calling app.getString(int)
for the format template and String.format() to apply args.
Simplifies test stubs to pure getString(int) returns.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
GET /api/messaging/platforms returns a JSON object with a platforms
array inside, but the app expected a bare array. Wraps the response
in a new MessagingPlatformResponse model.
- Adds MessagingPlatformResponse.kt wrapper model
- Updates HermesApiService.kt return type + adds profile query param
- Updates ChannelsViewModel.kt to unwrap .platforms
- Updates E2eIntegrationTest.kt mock to match new response shape
* ⚡ Optimize collection creation in Navigation.kt
Replaced uses of `.map { ... }.toSet()` with `.mapTo(mutableSetOf()) { ... }` in Navigation.kt. This avoids the creation of intermediate ArrayList objects, saving unnecessary memory allocations.
* ⚡ Optimize collection creation in Navigation.kt
Replaced uses of `.map { ... }.toSet()` with `.mapTo(mutableSetOf()) { ... }` in Navigation.kt. This avoids the creation of intermediate ArrayList objects, saving unnecessary memory allocations. Fixed ktlint formatting issues causing CI failure.
* feat: move session history to a dedicated history screen
Closes#159.
* fix(ci): fix ktlint style issues and unresolved NavigationController reference
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.