* feat(sessions): empty-session cleanup button + latest-descendant resume + fresh-session 404 fix (issues #787, #840)
- Sessions top bar: Delete-empty icon with count badge, grey/disabled when 0
(GET /api/sessions/empty/count + DELETE /api/sessions/empty, confirm dialog)
- Resume flow resolves GET /api/sessions/{id}/latest-descendant first —
branched sessions continue in the newest child leaf, not the stale parent
(best-effort; falls back to the requested id)
- syncCurrentSession guarded by sessionHasServerPresence — a freshly created
(never prompted) session has no server row yet; the REST transcript 404s
and the resume retry spun forever (logcat 404 evidence)
* fix(sessions): clamp message-count fallback to backend limit cap (422)
fetchServerMessageCount requested limit=500 but the backend caps
session list page size at 100 (sessions.py Query le=100) — every
fallback scan 422'd (seen in device logcat after a branch). Sessions
are ordered recent, so the target is always in the top page.
* test(slash): make dispatch-capture assertions order-independent (CI flake)
The capture-race flake (SlashCommandDispatchRpcTest, hit 3x across CI and
local runs 2026-08-07): tests asserted on methodSlot.captured — the LAST
request captured — but the create flow's session.usage/context_breakdown
requests can land after the slash-dispatch one depending on suite order
(the class's own comment documents this cross-wiring). Converted the
three capture-based tests to capture ALL requests and find the target
record by method, with dispatch-before-fallback ordering for the 4018
test. Same assertions, no dependence on request interleaving.
* test(slash): convert remaining capture-race assertions (flake round 2)
The capture race moved to the focus/init tests (:424) after the first
three were fixed — every request-capture test in the class used the
same last-captured assumption. Converted the remaining five (init,
init+arg, focus, focus on, type=send) to the find-record-by-method
pattern; the type=send answers block reads methodCalls.last() since the
capture precedes the answer. Zero request() slot captures remain.
* test(slash): blocklist assertion counts command RPCs only (flake round 3)
The blocklisted-command test counted ALL request() calls and asserted
zero — the create flow's session.usage/context_breakdown noise can land
in the window after the stub registers (the same capture race that hit
the dispatch tests). Asserted instead that no COMMAND_DISPATCH or
SLASH_EXEC fires, which is the actual blocklist contract. E2E's static
Dispatchers mock stays (E2E VMs need IO routed to the test dispatcher —
removing it broke 20 E2E tests).
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* fix: correct bulk session delete endpoint + surface deleted count
- Fix Retrofit @POST path from 'api/sessions/delete' to 'api/sessions/bulk-delete'
- Change response type from Response<Unit> to Response<BulkDeleteResponse>
- Add BulkDeleteResponse model (ok, deleted) matching backend contract
- Surface backend 'deleted' count in toast message instead of using client-side count
Backend contract (web_server.py:10750):
POST /api/sessions/bulk-delete
Body: { "ids": [...], "delete_all": false }
Response: { "ok": true, "deleted": <count> }
Fixes#651
* fix: replace client-side session list filter with full refresh after bulk delete
- Call loadSessions() + loadStats() after successful bulk delete instead of
filtering sessions/searchResults locally via 'ids' — fixes state desync
when backend deletes fewer sessions than requested (partial success) and
is correct for delete_all=true where ids may be empty
- Show 'No sessions were deleted' toast when deletedCount == 0
- Extract toastMsg to separate val to satisfy ktlint lint rules
Addresses review findings: UI state desync, delete_all handling, hardcoded
pluralizing, and zero-deletion UX.
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
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>
Removed docs/color-hardcoded-audit.md per request. Replaced 2 hardcoded
Color literals with theme tokens; added CI guard blocking new literals.
Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
Add ToastHost interface + override keyword to the 3 ViewModels that
declared clearToast() without implementing the interface. This aligns
them with the other 17 ViewModels and enables polymorphic substitution
where ToastHost is expected.
Fixes#614
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
* feat(#530): add SessionSearch models + searchSessions endpoint + contract tests
* feat(#530): wire server-backed search into Sessions screen (debounce + render)
* style(#530): ktlint import ordering on HermesApiService
* fix(#530): ktlint-clean overlap wrap + map search snippet to title
* fix(#530): map search snippet to title so results show + highlight
* fix(#530): render search hits as honest result cards instead of faking title
Backend SessionSearchResult carries no session name (only snippet + metadata),
so mapping snippet->title made the card show raw excerpt as the title. Instead,
render a dedicated SearchResultCard: 'Match' label + session id, highlighted
snippet as body, and source/model/played-at chips. Added SessionInfo.model and
a friendly relative-time formatter. ktlint clean, compileDebugKotlin + unit
tests pass.
* fix(#530): strip JSON from search snippet + tighten result card padding
* fix(#530): strip FTS highlight markers from search snippet on client
agy's backend change re-wraps matched terms in >>>term<<< markers inside the
clean snippet, but the mobile highlights by raw query term and never consumed
those markers — so literal '>>>'/'<<<' would render in result text. Strip the
markers before passing the snippet to highlightText so the query highlight
still works on clean text.
* fix(#530): integrate tree-flattening hierarchy with search modes in SessionsScreen
* fix(#530): fix test assertion and mock ApiClient in SessionsViewModelTest
* fix(#530): resolve compose PaddingValues compile error and fix slash-preservation test path
* fix(#530): clean search snippet from JSON payload and add unit test
---------
Co-authored-by: M57 <m57@hermes-control.app>
* 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 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>
* 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>
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
- 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
This commit addresses the accessibility issues described in Issue 296:
1. Replaced `pointerInput` gestures with `combinedClickable(role = Role.Button)` in `ChatBubble.kt` and `ChatScreen.kt` to ensure screen readers announce interactive text elements and chat bubbles correctly.
2. Added `role = Role.Button` to the clickable modifiers in `SessionsScreen.kt`.
3. Introduced proper `testTag` modifiers throughout interactive components like `session_card`, `chat_bubble_user`, `chat_bubble_assistant`, and `clarify_option` to facilitate UI testing.
* 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>
* feat: move session history to a dedicated history screen
Closes#159.
* fix(ci): fix ktlint style issues and unresolved NavigationController reference