ea8fead087f0f33e8034e13fdf294ff8e678e673
99
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
57dc4cdf5c |
fix(sessions): Messages stat card + slim prune button (#855)
* fix(sessions): show Messages stat + slim prune button The Active stat card always rendered 0: the mobile model declared 'val active: Int = 0', a key GET /api/sessions/stats never sends (backend returns total/active_store/archived/messages/by_source), and with ignoreUnknownKeys = true deserialization silently fell back to 0. Rework the stats row instead of just key-mapping: - Messages card replaces Active, reading the 'messages' key (db.message_count(), web_routers/sessions.py:523-550) - Prune becomes a slim 52dp icon-only card (contentDescription keeps it accessible) so the Messages card keeps width for large counts * feat(sessions): compact stat counts (100k / 1.23m) for Total & Messages formatCompactCount renders counts as max 3 digits + unit letter: 987 -> '987', 100987 -> '100k', 1234567 -> '1.23m', 100000000 -> '100m'. Applied to both the Total and Messages stat cards so large numbers fit the row. Covered by SessionsStatsFormatTest (5 test cases). * fix(sessions): label prune + empty-cleanup buttons, drop top select toggle - Prune card now sizes to its label (icon + 'Prune' text) instead of a 52dp icon-only square, staying compact via wrap-content width. - Empty-session cleanup becomes a labeled FilledTonalButton (icon + 'Delete empty sessions', count badge kept) instead of a bare icon. - Removed the top-bar select toggle (SelectAll icon); selection mode remains reachable via long-press > Select on any session row. - Dropped now-orphaned content_desc_enter/exit_selection strings (en/ko). * fix(sessions): prune card wrap-content width + shorter cleanup label The prune card dropped its fixed 52dp width but kept fillMaxSize on the inner Box — as an unweighted Row child that expands to the full row width, starving the weighted Total/Messages cards (reported broken on device). The Box now fills height only, so the Card wraps to its content (icon + 'Prune'). Empty-session cleanup button label shortened to 'Delete empty'. * fix(sessions): stack prune label under icon, cleanup button reads 'Empty' - Prune card: trash icon above the 'Prune' label (column layout), still wrap-content width so Total/Messages keep their space. - Empty-session cleanup button label shortened to 'Empty'. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
ab42e19b7f |
theme(849): unify presets behind one palette template + bespoke AMOLED status colors (#852)
* refactor(theme): unify all presets behind one palette template * docs(theme): update THEMES.md and AGENTS.md for the palette template * fix(theme): restore per-preset error slot contrast, merge StatusColors into HermesStatusColors - onErrorContainer becomes an explicit per-preset field (was derived from onError — same-color pairs in Gruvbox/Catppuccin light made error text invisible; 6 screens render this slot) - Default dark + AMOLED onError -> White (were <3:1 on their error color) - drop StatusColors + statusColorsOf mapper; PaletteColors.status is typed HermesStatusColors directly (identical field lists, pure copy ceremony) - ThemePalette getters now by lazy; schemeFor/statusFor accessors; requireNotNull on the default-theme fallback (DefaultTheme is FULL) - resolvers internal for testability * test(theme): contrast, mode-validation, and fallback contract tests - every shipped mode's onError/error + onErrorContainer/errorContainer pair meets >= 3:1 WCAG contrast (kills the error-slot regression class) - ThemeMode init validation rejects illegal palette combinations - AMOLED darkOnly contract + light-mode fallback to Default light * refactor(theme): Catppuccin — named swatches from the official palette spec Hex values verified identical (zero color drift): repeated colors (Crust, Surface0/1, Base, Text...) now live once as named swatches matching catppuccin.com; bespoke light primary container tints stay local. Mocha/Latte pair unchanged. * refactor(theme): Gruvbox — authentic bright/faded accent scheme with named swatches Redesign per gruvbox spec: bright accents on dark bg0-bg4, faded accents on light bg0-bg4 (correct contrast play for light mode). Named swatches, single Gray shared across modes. Dark error pair fixed for >= 3:1 (onErrorContainer -> DarkFg1 cream, was bright red on Bg3 = 1.87:1). * feat(theme): replace default preset with Slate — modern slate blues + teal accents The brand default (voltage purple/amber) becomes Slate: bright teal/blue accents on dark bg0-bg4, faded variants on light bg0-bg4. Named swatches, single neutral gray shared across modes. Slate is also the fallback for darkOnly/lightOnly presets (AMOLED light mode). * feat(theme): add Nord preset — Polar Night/Snow Storm surfaces, Frost/Aurora accents Nord's single accent set is reused across both modes (pastel accents stay light, so on* colors use Polar Night dark text in both modes — documented in-file). Surface ladder shares outer tiers (Nord ships 4 dark + 3 light tones for 5 tiers). Dark error pair fixed for >= 3:1 (onErrorContainer -> Nord6, was Nord11 on Nord2 = 2.0:1). Wired into ThemePreset, the settings picker, and docs; default label now reads Default (Slate). * refactor(theme): Monochrome + AMOLED — pure grayscale ramp, true-black AMOLED variant Monochrome: every swatch R=G=B, roles by lightness only (status colors included — documented trade-off: pair with icons/labels). AMOLED: dark-only true-black variant (pure #000000, OLED pixels off; #121212 baseline of Monochrome doesn't do that), containers collapse where nothing darker exists. Both ship maximum-contrast grayscale pairs (12.8:1+ on error slots). * chore(theme): drop Neon Noir, finalize docs + picker label - remove NeonNoirScheme.kt + ThemePreset.NEON_NOIR everywhere (dispatcher, picker, strings EN/KO, tests, AGENTS.md, THEMES.md) — 6 presets remain: Default, Monochrome, Gruvbox, Catppuccin, AMOLED, Nord - picker label for the default preset is now plain 'Default' - THEMES.md: preset conventions section (named swatches, bright/faded split, grayscale-status caveat, contrast test gate) + verify step runs testDebugUnitTest * fix(theme): make drawer icon colors visibly follow the active preset Selected icon now tints with the theme's primary accent (teal for Slate, orange for Gruvbox, mauve for Catppuccin, frost-blue for Nord) instead of onPrimaryContainer — the new presets' containers are all dark gray with near-white text, so the selected row looked identical across themes. Unselected icon/text colors pinned explicitly to onSurfaceVariant. * chore(ui): remove lightning emoji from the app - drawer title + About name: '⚡ Hermes' -> 'Hermes' (EN + KO) - skill markers (chat self-improvement badge, memory graph): ⚡ -> 🛠 - in-progress indicators (subagent inspection): ⚡ -> 🔄 --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
f20b50b48f |
feat(sessions): empty-session cleanup + latest-descendant resume + fresh-session 404 fix (#787, #840) (#841)
* 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> |
||
|
|
7998248507 |
feat(system): multipart backup import via SAF picker — replace path-based import (issue #786) (#839)
* feat(system): multipart backup import via SAF picker — replace path-based import (issue #786) * fix(system): backup trigger sends required JSON body — 422 fix (BackupRequest contract) * fix(system): backup download waits for async archive + saves to Downloads * fix(system): curator min_idle_hours Double parse + single-flight backup download * fix(system): stream backup download to Downloads — @Streaming + chunked MediaStore write (OOM fix) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
9d0baa78fd |
fix(sessions): rename via PATCH /api/sessions/{id} + move actions to hold menu (issue #785) (#838)
Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
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> |
||
|
|
fd5d851405 |
feat(plugins): memory provider config & setup management (issue #783) (#834)
* feat(plugins): memory provider config & setup management (issue #783) * fix(memory): remove top padding on provider detail screen --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
fe79ff42b4 |
feat(toolsets): per-toolset config/env/provider/post-setup management (#782) (#831)
* feat(toolsets): per-toolset config/env/provider/post-setup management (#782) * fix(toolsets): per-entry toolset name for shared detail ViewModel (#782) NavDisplay uses the activity-level ViewModelStoreOwner, so class-keyed viewModel{} returns the same ToolsetDetailViewModel for every detail entry; the constructor-captured name froze the first-opened toolset into all entries (on-device: every toolset showed web's providers, no further /config calls after the first). The screen now drives setToolset(name) + loadConfig() per entry, and setToolset resets per-toolset state (config, revealed secrets, running post-setup) so nothing bleeds across toolsets. * style(toolsets): single-line scrollable tool chip strip (#782) A wrapping FlowRow stacked chips vertically for toolsets with many tools (web etc.), ballooning card height — especially on large font scales. Chips now sit on one horizontally scrollable row. * chore(toolsets): drop stray terminal-noise line from chip strip change --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
442e868ce2 |
feat(cron): blueprint start-from + delivery target picker in job editor (issue #780) (#822)
- New-job dialog gains a Start-from dropdown: pick a catalog blueprint, fill its typed slots (time/enum/weekdays/text), save instantiates it via POST /api/cron/blueprints/instantiate - Delivery field is now an editable picker fed by GET /api/cron/delivery-targets (origin + local + connected platforms) - Blueprints and targets fetched on editor open; 422 slot errors surface the backend detail Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
80be168801 |
feat(profiles): delete, rename, auto-describe and setup command (issue #781) (#818)
* feat(profiles): delete, rename, auto-describe and setup command (issue #781) * feat(profiles): use shared ModelPickerDialog for set model (issue #781) * fix(profiles): move overflow menu to card header so it can't be clipped (issue #781) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
1870b375b1 |
feat(skills): hub sources + featured landing and security scan (#779) (#793)
Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
00a0eedc77 |
feat(logs): server-side file/level/component/line filters (#777) (#792)
Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
5870b73397 |
feat(#776): pairing management screen (#791)
REST + Channels-style UI for DM pairing (list pending/approved, approve, revoke, clear). Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
44767fef4a |
feat(kanban): live board updates over plugin events WebSocket + kanban UI pass (#789)
* 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> |
||
|
|
1e8c3629f0 |
chore: rename launcher label to 'Hermes Mobile' (#770)
Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
5e1ee0b4e5 |
feat(chat): desktop-parity session resume — warm cache + bounded retry (#766)
Bring the desktop's session reconnection UX to mobile: - Warm-cache fast-path: switchSession paints the cached Room transcript immediately instead of blanking the screen, then refreshes from the server in parallel (stale cache can't clobber a fresher server page) - Bounded auto-retry: a failed session.resume (WS reject or REST transcript failure) retries with exponential backoff 1s->2s->4s->8s, capped at 4 attempts; WS + REST failures in the same cycle count as one - Explicit error recovery: after exhaustion the chat shows an error card with a manual Retry instead of latching the spinner forever; manual retry resets the backoff cycle - Reconnect-aware resume: gateway (re)connect clears stale error state and re-resumes the current session on the new socket - Unit tests: warm-cache paint, bounded retry exhaustion, WS+REST dedupe, manual retry reset, reconnect restart Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
ed48e10ca6 |
feat: register as Android default digital assistant (#757) (#758)
* feat: register as Android default digital assistant
* fix: assistant role request fallback + always-attempt click
* remove: assistant role request row from Settings
The system-level assistant registration stays (VoiceInteractionService
+ ROLE_ASSISTANT, set via system Settings -> Default apps), but the
in-app Settings row is dropped: RequestRoleActivity fails to resolve
the caller on a plain startActivity ('Package name cannot be null or
empty'), so the button was a dead end. Removes the row, the
AssistantRole helper, and the now-unused strings.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
|
||
|
|
422bb9f5e6 |
feat: support hosted OAuth for authenticated HTTP MCP servers (#751) (#752)
* feat: support hosted OAuth for authenticated HTTP MCP servers (#751) * fix(mcp): import McpOAuthFlowResponse in McpServersViewModel --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
1b596746af |
feat(chat/activity): Self-improvement event badges for memory & skill updates (#744)
* feat(chat/activity): Self-improvement event badges for memory & skill updates (#737) * feat(chat): Handle background review.summary WS events for self-improvement notices --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
f45a68b0db |
feat(chat): full-screen image viewer (issue #723) (#729)
* feat(chat): full-screen image viewer (issue #723) Tap any chat image (markdown , inline attachment thumbnail, or bubble image) to open a full-screen viewer: - pinch-zoom + pan via native detectTransformGestures - downswipe-to-dismiss at min zoom - Save -> device Downloads (MediaStore), Share -> Android share sheet (both fully device-local; never sent back to the Hermes server) Plumbing: onImageClick(ImageViewerModel) flows MarkdownText -> ChatBubble/InlineAttachment -> ChatMessageList -> ChatScreen (state + ImageViewerDialog). ImageBytesResolver reuses the existing authed OkHttpProvider path for gateway http(s) sources; data:/content:// handled locally. No new dependencies. Closes #723 * fix(chat): resolve strings at composable scope to satisfy LocalContextGetResourceValueCall lint Lint forbids reading resource *values* from LocalContext.current inside coroutine lambdas. Hoist all viewer strings via stringResource() at composable scope and reuse them in onSave/onShare. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
6aee02c4da |
feat: add Files tab backed by the managed-files endpoint (#727)
* feat: add Files tab backed by the managed-files endpoint Add a Hermes Mobile tab that browses the gateway's managed-files root via the existing /api/files* backend routes: list/navigate folders, breadcrumb path, create folder, upload (streaming multipart), delete, and open files via the authenticated GatewayFileClient download URL. - data/model/ManagedFile: entry + list/read/upload/mkdir/delete models - HermesApiService: list/read/upload(+stream)/mkdir/delete endpoints - FilesViewModel: state, navigation, per-row busy, transient-state cleanup - FilesScreen: folder list, breadcrumb, FAB new folder, upload picker, delete confirm, open via system viewer - NavigationKeys + ScreenRegistry + strings: register in Inspect drawer section * fix(files): open files in-app via FileProvider instead of browser URL The previous file-open path handed a ?token=-authenticated URL to the system browser, but /api/files/download only accepts the ephemeral in-process session token as ?token=, not the mobile's Bearer token — so the request failed and the browser rendered the JSON error body instead of the file. Fix: download the file in-app through the authenticated client (Bearer/ session-cookie, same path as the rest of the app), decode the base64 data_url, write it to cacheDir, and open it via FileProvider.getUriForFile + ACTION_VIEW (mirrors the existing ChatScreen/SystemViewModel open-from-cache pattern). Works on a remote phone since the file is materialized locally first. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
0180fb12c2 |
Add used/full context meter to chat screen (#717)
* Adopt /api/health REST route (issue #713) Cheap liveness probe on the connection screen: GET /api/health surfaces backend version + auth mode (Token vs Gated/OAuth) without the heavier /api/status payload. Added HermesApiService.getHealth + HealthStatus model; ConnectViewModel.loadHealth probes on open (best-effort, silent on failure); ConnectScreen shows a read-only health card when reachable. Verified: ktlint clean, assembleDebug BUILD SUCCESSFUL locally. * Add used/full context meter to chat screen Shows session prompt tokens vs model context window above the composer, polled every 5s. Color-coded bar warns at 70%/90%. * fix(chat): fix unit tests for context usage meter Format >=100k tokens cleanly in ContextUsageChip and stub AuthManager profile ID in ChatViewModelTest. * Fix context meter: source used tokens from input_tokens Gateway /api/sessions/{id} has no last_prompt_tokens column; use input_tokens instead. Show meter as soon as full context is known. * Add tappable context breakdown sheet Chip opens a bottom sheet with input/output/cache/read/write/reasoning token rows + message count. Source breakdown from /api/sessions/{id}. * Refresh context meter on streaming complete Trigger fetchContextUsage immediately when a message finishes streaming instead of waiting for the 5s session-sync poll. * Fix context meter: source used tokens from live WS session.info The REST /api/sessions/{id} endpoint reads the DB input_tokens column, which is stale/0 for active sessions — that's why the meter was stuck at 0. Switch the meter to the live values the gateway pushes over the WS session.info payload after each turn: - used = last_prompt_tokens (real used context for the turn) - full = context_length (model-accurate window; keep /api/model/info as a fallback before the first turn completes) fetchContextUsage now only tops up the denominator from /api/model/info and no longer overwrites the live WS value with the stale REST input_tokens. The 5s poll remains as a fallback. * Revert "Fix context meter: source used tokens from live WS session.info" This reverts commit 932393fd80993d57e66fe93552a949460d646674. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
9089b4f8ac |
feat(keys): overhaul Keys screen UI and UX (#707)
* feat(keys): overhaul Keys screen UI and UX Fix card padding alignment, add delete confirmation dialog, add FAB with AddKeyDialog, support copying revealed values, add status badges, and pin restart banner. * fix(keys): fix scaffold actions parameter and missing string resource Resolve compilation errors in KeysScreen by removing floatingActionButton from HermesScaffold and adding missing keys_action_open_url resource. * feat(keys): replace dropdown headers with horizontal category filter chips Add CategoryFilterBar featuring horizontal FilterChips for quick category filtering (All, LLM Providers, Tool API Keys, Messaging Platforms, Agent Settings) with total key counts. * style(keys): enhance key card padding, spacing, borders, and surface contrast Use surfaceContainerLow with subtle outlineVariant border and 16dp rounded corners for high contrast cards, and set 12dp list spacing between cards. * fix(keys): wrap key title and status badge in FlowRow to prevent layout overflow on long key names Prevent status badge from squishing or overflowing when displaying long API key names by using a FlowRow that wraps gracefully. * fix(keys): add file-level OptIn for ExperimentalLayoutApi Fix compilation error by adding @file:OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) to KeysScreen.kt. * fix(keys): fully qualify class names in file-level OptIn annotation Fix unresolved reference build error by fully qualifying ExperimentalMaterial3Api and ExperimentalLayoutApi in @file:OptIn before package statement. * fix(keys): remove invalid verticalAlignment parameter from FlowRow * fix(keys): optimize long API key name readability and wrapping Insert zero-width spaces after underscores to ensure clean word-boundary wrapping, add adaptive typography (titleSmall with 20sp line height for long names), and stack StatusBadge cleanly below the title. * feat(keys): make API key names and values single-line and horizontally scrollable Enforce singleLine=true / maxLines=1 with horizontalScroll(rememberScrollState()) for both key names and revealed key values so cards remain compact with uniform heights. * feat(keys): keep single-line scrollable key values and clean multi-line key names Keep secret key values single-line and horizontally scrollable, while restoring clean multi-line word-boundary breaking (at underscores) for key names. * style(keys): format KeysScreen.kt with ktlint |
||
|
|
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> |
||
|
|
0edb67338d |
refactor: remove bottom navigation bar (#665 #666 #667 #668) (#672)
* refactor(nav): remove bottom NavigationBar from Scaffold (#665) * refactor(theme): remove BottomNavDisplayMode enum (#666) * refactor(settings): remove nav bar customization page (#667) * chore: clean up unused imports after bottom nav removal (#668) * chore: remove dead bottom nav infrastructure after removal - Remove SettingsNavBar NavKey (NavigationKeys.kt) - Remove bottomNavItems field from ServerStoreState + migration code - Remove _bottomNavItemsFlow, getBottomNavItems, setBottomNavItems from AuthManager - Remove bottomNavItems handling from selfHealed() (ServerCollectionOps) - Remove redundant bottomBar={} from Scaffold (Navigation.kt) - Remove unused nav bar strings from values/strings.xml + values-ko/strings.xml - Remove content_desc_move_up/down/remove_item strings - Update SettingsViewModelTest.kt (drop stale getBottomNavItems mock) - Update ServerStoreTest.kt (remove bottomNavItems assertions) --------- Co-authored-by: M57 <Hy4ri@users.noreply.github.com> |
||
|
|
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> |
||
|
|
367d6e22f5 | fix: language selector | ||
|
|
26b104b2bb |
[feature] issue #634: HTTPS + reverse-proxy server endpoint support (#638)
* [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> |
||
|
|
3363c1e586 |
perf(analytics): preload on launch + split-fetch + disk cache (issue #537) (#630)
* feat(billing): adopt subscription.* + usage.bars WS methods (issue #628) Add Billing/Plan screen + WS client surface for the backend release audit (hermes-agent 0bf44d557..614dc194e). credits.view was removed upstream; subscription.{state,preview,change,resume,upgrade} + usage.bars replace it. - WsMethods: add SUBSCRIPTION_* + USAGE_BARS constants - BillingModels: @Serializable request/response (snake_case, ignoreUnknownKeys) - BillingRepository: WS dispatch via HermesWsClient.request + typed decode; HermesRpcException (-32601) surfaces as featureUnavailable, not a crash - BillingViewModel: plan/usage load + upgrade/change/resume/cancel actions - BillingScreen: HermesScaffold composable (plan card, usage bars, actions) - NavigationKeys + ScreenRegistry + strings (en/ko) wire BillingScreen into the INSPECT drawer section Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL. * fix(billing): align models + UI to live gateway wire shape (issue #628) Live-probed the 6 subscription.* / usage.bars RPCs against the running backend (hermes-agent dashboard :9119, 2026-07-19). Findings: - subscription.state returns logged_in / current / tiers / usage (nested), NOT the legacy subscription_type_id / renews_at fields the models assumed. - Mutating RPCs (preview/change/resume/upgrade) return ok:false *results* with error/message/payload (e.g. invalid_token when not portal-authed), not RPC-level errors — so the ViewModel's HermesRpcException guard never fired and the screen silently half-decoded null fields. - subscription.upgrade also returns an idempotency_key not in the model. Changes: - BillingModels: re-shape SubscriptionStateResponse around current/tiers/ usage; add ok/error/message/payload envelope + idempotency_key to the action responses; add SubscriptionCurrent / SubscriptionTier / preview payload types. All nullable + ignoreUnknownKeys (graceful degrade). - BillingViewModel: detect ok:false results on every call and route to featureUnavailable / errorMessage instead of swallowing them. - BillingScreen: read plan from subscription.current; render a clean 'no active plan' / 'log in to Nous Portal' card on the logged-out path instead of a broken null plan card. - strings: add billing_no_active_plan / billing_login_required (en + ko). Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL. * fix(billing): address independent review (agy) — error routing + icon Triage of the independent AGY review on PR #629: Fixed (real issues): - BillingViewModel: usage.bars RPC failure (-32601) no longer marks the whole feature unavailable (was blocking the plan card despite the 'non-fatal' comment); it is now correctly non-fatal. - BillingViewModel: a failed primary subscription load now surfaces errorMessage even when usage bars loaded (was swallowed by 'sub == null && bars == null'). - BillingScreen: render state.errorMessage as an error card inside BillingContent so failed mutating actions (upgrade/cancel) are visible instead of silently disappearing. - BillingScreen: wrap the dead AccountBalanceWallet expression in a real Icon() composable in FeatureUnavailableState. Declined (with rationale, recorded for maintainer): - Tier-select / upgrade UI (review issue #4): explicitly out of scope per the PR body — 'Plan-selection UI ... is not built ... left for a follow-up once backend returns the available plans.' - Rename snake_case Kotlin props to camelCase (review nit #2): incorrect — OkHttpProvider uses JsonNamingStrategy.SnakeCase, which maps snake_case wire keys onto snake_case properties; the props are correct. Fixed the misleading doc comment instead. * fix(billing): match REAL logged-in wire shapes + kill null-result crash The live gateway probe (logged-in Nous Portal session, 2026-07-19) showed the earlier models assumed the WRONG shape: - subscription.state.current is {tier_id, tier_name, monthly_credits, credits_remaining, cycle_ends_at, pending_downgrade_*} — NOT the legacy subscription_type_id/status/renews_at fields. Screen rendered 'Free / no plan' for a real logged-in user. - usage.bars is {available, status, plan_name, plan_bar{remaining_display, total_display, pct_used, fill_fraction}, topup_bar} — NOT a List<UsageBar>. - Mutating RPCs return error:'insufficient_scope' when the Portal token lacks billing:manage scope (ok:false result, not an RPC error). Crash fix (root cause of 'Billing RPC returned no result payload'): - BillingRepository.decode() now returns null on a missing/null result instead of requireNotNull() -> IllegalStateException. All repo methods return nullable; ViewModel handles null as a non-fatal 'No billing data returned' instead of crashing. - BillingScreen.PlanCard / UsageBarRow rewritten to the real fields; the resume/cancel branch now gates on can_change_plan (no status field exists on current anymore). - UsageBarsResponse + UsageBar reshaped to plan_bar/topup_bar; added billing_credits_remaining / billing_plan / billing_topup strings (en+ko). Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL. Decode sim against the captured live responses confirms PlanCard/UsageBarRow/error paths all resolve with no null-crash. * fix(billing): decode WS result that arrives as toAny() Map, not JsonElement Root cause of 'No billing data returned' on a logged-in server: the backend returns a valid subscription.state result, but HermesWsClient.request() yields result already converted via JsonElement.toAny() (a Map<String,Any?>), not a raw JsonElement. BillingRepository.decode() only accepted JsonElement, so was null and the repo returned null -> ViewModel showed the generic 'No billing data returned' message. The server DOES have billing (logged_in:true, tier_name:Free, /usr/bin/bash.10 credits) — the app just couldn't parse it. Fix: decode() now normalizes whatever arrives — a kotlinx JsonElement OR the toAny() Map/List — back into a JsonElement via a recursive Any->JsonElement walk, then deserializes. Both shapes decode correctly now. This also means the previous 'Billing RPC returned no result payload' crash and the generic 'no data' message were both symptoms of this same parse gap, not a missing backend feature. Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL. * perf(analytics): preload on launch + split-fetch + disk cache (issue #537) Cut perceived analytics-tab latency for every user regardless of backend DB size: A. Preload analytics (7/30/90) in the background on app launch (AnalyticsPreloader) so the tab renders instantly when opened. B. Split the two endpoint fetches: /models (~0.1s, carries the totals card) now renders immediately while the slow /usage (insights engine over full message history) streams in behind a placeholder. C. Persist last successful response to DataStore so a cold app launch serves last session's data instantly, then refreshes (AnalyticsCacheStore). Root cause of the slowness is server-side (/api/analytics/usage cold read of a multi-GB state.db); these client changes make the common case instant and never block the UI on the slow call. * fix(analytics): address agy review — null-safety, profile-keyed cache, no main-thread block Triage of independent agy review (High model) on PR #630: Fixed (real): - Blank-on-cold-launch: AnalyticsContent early-returned on null usage, blanking the tab while the slow /usage call loaded. Now renders the fast /models half + placeholders; usage-only cards (totals, daily chart) and Skills/Tools tabs guard on usage != null. - Profile isolation: cache was keyed by days only -> cross-profile data leak on switch/cold-start. Now keyed "$days:$profile". - Main-thread block: ViewModel.init used runBlocking(IO) on the UI thread at startup. Moved disk-seed into viewModelScope.launch. - Serialization parity: cache used bare kotlinx Json; switch to OkHttpProvider.json (ignoreUnknownKeys) so a backend field addition can't break on-disk decode. Declined (not introduced by this PR / protected by viewModelScope): - "safeApiCall swallows CancellationException": pre-existing NetworkResult.kt behavior; viewModelScope cancellation already prevents post-clear state updates. Out of scope for this change. * fix(analytics): crash on tab open — LocalContext is Activity, not Application AnalyticsScreen cast LocalContext.current as Application, but under Compose the composition context is the *Activity*, so the cast threw ClassCastException the moment the tab composed (on tap). Use .applicationContext, which resolves to the Application instance. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
00c0ddfb92 |
feat(billing): adopt subscription.* + usage.bars WS methods (issue #628) (#629)
* feat(billing): adopt subscription.* + usage.bars WS methods (issue #628) Add Billing/Plan screen + WS client surface for the backend release audit (hermes-agent 0bf44d557..614dc194e). credits.view was removed upstream; subscription.{state,preview,change,resume,upgrade} + usage.bars replace it. - WsMethods: add SUBSCRIPTION_* + USAGE_BARS constants - BillingModels: @Serializable request/response (snake_case, ignoreUnknownKeys) - BillingRepository: WS dispatch via HermesWsClient.request + typed decode; HermesRpcException (-32601) surfaces as featureUnavailable, not a crash - BillingViewModel: plan/usage load + upgrade/change/resume/cancel actions - BillingScreen: HermesScaffold composable (plan card, usage bars, actions) - NavigationKeys + ScreenRegistry + strings (en/ko) wire BillingScreen into the INSPECT drawer section Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL. * fix(billing): align models + UI to live gateway wire shape (issue #628) Live-probed the 6 subscription.* / usage.bars RPCs against the running backend (hermes-agent dashboard :9119, 2026-07-19). Findings: - subscription.state returns logged_in / current / tiers / usage (nested), NOT the legacy subscription_type_id / renews_at fields the models assumed. - Mutating RPCs (preview/change/resume/upgrade) return ok:false *results* with error/message/payload (e.g. invalid_token when not portal-authed), not RPC-level errors — so the ViewModel's HermesRpcException guard never fired and the screen silently half-decoded null fields. - subscription.upgrade also returns an idempotency_key not in the model. Changes: - BillingModels: re-shape SubscriptionStateResponse around current/tiers/ usage; add ok/error/message/payload envelope + idempotency_key to the action responses; add SubscriptionCurrent / SubscriptionTier / preview payload types. All nullable + ignoreUnknownKeys (graceful degrade). - BillingViewModel: detect ok:false results on every call and route to featureUnavailable / errorMessage instead of swallowing them. - BillingScreen: read plan from subscription.current; render a clean 'no active plan' / 'log in to Nous Portal' card on the logged-out path instead of a broken null plan card. - strings: add billing_no_active_plan / billing_login_required (en + ko). Verified: ./gradlew ktlintCheck + compileDebugKotlin both BUILD SUCCESSFUL. * fix(billing): address independent review (agy) — error routing + icon Triage of the independent AGY review on PR #629: Fixed (real issues): - BillingViewModel: usage.bars RPC failure (-32601) no longer marks the whole feature unavailable (was blocking the plan card despite the 'non-fatal' comment); it is now correctly non-fatal. - BillingViewModel: a failed primary subscription load now surfaces errorMessage even when usage bars loaded (was swallowed by 'sub == null && bars == null'). - BillingScreen: render state.errorMessage as an error card inside BillingContent so failed mutating actions (upgrade/cancel) are visible instead of silently disappearing. - BillingScreen: wrap the dead AccountBalanceWallet expression in a real Icon() composable in FeatureUnavailableState. Declined (with rationale, recorded for maintainer): - Tier-select / upgrade UI (review issue #4): explicitly out of scope per the PR body — 'Plan-selection UI ... is not built ... left for a follow-up once backend returns the available plans.' - Rename snake_case Kotlin props to camelCase (review nit #2): incorrect — OkHttpProvider uses JsonNamingStrategy.SnakeCase, which maps snake_case wire keys onto snake_case properties; the props are correct. Fixed the misleading doc comment instead. * fix(billing): match REAL logged-in wire shapes + kill null-result crash The live gateway probe (logged-in Nous Portal session, 2026-07-19) showed the earlier models assumed the WRONG shape: - subscription.state.current is {tier_id, tier_name, monthly_credits, credits_remaining, cycle_ends_at, pending_downgrade_*} — NOT the legacy subscription_type_id/status/renews_at fields. Screen rendered 'Free / no plan' for a real logged-in user. - usage.bars is {available, status, plan_name, plan_bar{remaining_display, total_display, pct_used, fill_fraction}, topup_bar} — NOT a List<UsageBar>. - Mutating RPCs return error:'insufficient_scope' when the Portal token lacks billing:manage scope (ok:false result, not an RPC error). Crash fix (root cause of 'Billing RPC returned no result payload'): - BillingRepository.decode() now returns null on a missing/null result instead of requireNotNull() -> IllegalStateException. All repo methods return nullable; ViewModel handles null as a non-fatal 'No billing data returned' instead of crashing. - BillingScreen.PlanCard / UsageBarRow rewritten to the real fields; the resume/cancel branch now gates on can_change_plan (no status field exists on current anymore). - UsageBarsResponse + UsageBar reshaped to plan_bar/topup_bar; added billing_credits_remaining / billing_plan / billing_topup strings (en+ko). Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL. Decode sim against the captured live responses confirms PlanCard/UsageBarRow/error paths all resolve with no null-crash. * fix(billing): decode WS result that arrives as toAny() Map, not JsonElement Root cause of 'No billing data returned' on a logged-in server: the backend returns a valid subscription.state result, but HermesWsClient.request() yields result already converted via JsonElement.toAny() (a Map<String,Any?>), not a raw JsonElement. BillingRepository.decode() only accepted JsonElement, so was null and the repo returned null -> ViewModel showed the generic 'No billing data returned' message. The server DOES have billing (logged_in:true, tier_name:Free, /usr/bin/bash.10 credits) — the app just couldn't parse it. Fix: decode() now normalizes whatever arrives — a kotlinx JsonElement OR the toAny() Map/List — back into a JsonElement via a recursive Any->JsonElement walk, then deserializes. Both shapes decode correctly now. This also means the previous 'Billing RPC returned no result payload' crash and the generic 'no data' message were both symptoms of this same parse gap, not a missing backend feature. Verified: ktlint clean + compileDebugKotlin BUILD SUCCESSFUL. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
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> |
||
|
|
d484d535ce |
fix(#613): remove 77 unused string resources (#616)
Independently verified dead weight: computed defined vs used set across all source sets (R.string.* in Kotlin + @string/* in XML/manifest). Zero references in app/src/main, androidTest, or test fixtures. - values/strings.xml: -77 unused <string> entries (818 -> 741) - values-ko/strings.xml: -67 corresponding entries (704 -> 637) Build: ./gradlew assembleDebug BUILD SUCCESSFUL (no missing-resource errors). True KO translation gap after prune = 741 used EN - 637 translated KO = 104 (previously mis-stated as 114; the 67 removed were never translated anyway). Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
228431d57a |
[enhancement] feat(#606): support auth mode + bearer token when adding HTTP MCP servers (#607)
* feat(#606): support auth mode + bearer token when adding HTTP MCP servers - Extend AddMcpServerRequest with auth (none|header|oauth) and bearerToken - Add auth mode FilterChip selector in HTTP add-server dialog - Conditionally show masked bearer token field when auth == header - Wire new fields through McpServersViewModel request builder Backend contract: exactly one of url/command required; bearer_token requires auth == "header"; args/env only for stdio. SnakeCase naming strategy auto-serializes bearerToken -> bearer_token. Fixes #606 * fix(#606): address agy review — remember PasswordVisualTransformation + FlowRow for auth chips - Wrap PasswordVisualTransformation in remember {} to avoid per-recomposition allocation - Use FlowRow for the 3 auth-mode FilterChips so they wrap on narrow screens instead of overflowing |
||
|
|
3e357594be |
refactor(settings): drill-down navigation replacing tabbed layout (#605)
* refactor(settings): drill-down navigation replacing tabbed layout Replaces the 4-tab settings layout with a native Android-style drill-down: a single category list (SettingsScreen) where each row pushes a dedicated sub-page onto the Navigation3 back stack (SettingsConnection / Appearance / Chat / NavBar / Behavior / About). Each sub-page is its own NavKey destination registered in Navigation.kt and reuses the existing section composables. - SettingsScreen becomes the category list with icon + label + summary (Appearance row shows live theme + language, e.g. "Dark · 한국어"). - New SettingsSubPages.kt hosts the 6 full-screen sub-pages. - AboutSection promoted out of SettingsScreen into its own sub-page. - SettingsViewModel remains the single source of truth; no persistence changes. - Adds settings_summary_* strings for list subtitles. Consumes the in-app language switcher from the prior commit (AppearancePage passes appLanguage through to AppearanceSection). * [bug] fix settings sub-page back button opening drawer on first tap The ModalNavigationDrawer wraps the whole app and stays gesture-enabled on the Settings root (a DRAWER_GESTURE_SCREEN). Drilling into a sub-page (SettingsConnection/Appearance/Chat/NavBar/Behavior/About) disables gestures, but the existing LaunchedEffect only snapped the drawer closed when it was already isOpen. The drawer's scrim surface lingered during the transition and intercepted the first back-button tap, sliding the sidebar open instead of navigating back. Now snapTo(Closed) unconditionally whenever gestures are disabled — a safe no-op when already closed — so the sub-page back button works on the first tap. * [bug] fix settings sub-page back button opening drawer (gesture-surface race) The first "back" tap on a settings sub-page opened the sidebar instead of navigating back. Root cause: Settings root is a gesture-enabled drawer screen, and the sub-pages are the first destinations ever drilled down from it. When navigating in, ModalNavigationDrawer's gesture interceptor surface lingered for a frame (gesture -> non-gesture transition), so the top-left tap was consumed by the drawer instead of the Back IconButton. The previous snapTo fix only reset drawer *state*, not the gesture layer, so it didn't help. Fix: NavigationController now holds a closeDrawer hook (set by MainNavigation) and invokes it synchronously inside navigateTo() when pushing a non-primary sub-page. The drawer is dismissed in the same tap handler that triggers the navigation, before any recomposition, so the sub-page is never tappable while the gesture surface is still mounted. The LaunchedEffect(gesturesEnabled) snapTo remains as a backstop for non-navigateTo entries. * fix(#603): delay drawer gesture activation to prevent residual swipe * fix(#603): fix settings back navigation and drawer auto-open --------- Co-authored-by: hy4ri <hy4ri@users.noreply.github.com> |
||
|
|
b3765bd106 |
feat: in-app display language switcher (Settings → Appearance) (#604)
Add a Language selector to Settings → Appearance that overrides the app display language independent of the device locale. Persists a BCP-47 code in ServerStoreState.appLanguage; MainActivity.attachBaseContext applies it via a ContextWrapper (works on minSdk 26, no AppCompat dependency needed since the Activity is a plain ComponentActivity). Selecting a language recreates the activity so resources re-resolve immediately. Includes System default / English / 한국어 options, plus a unit test for the locale-code resolver. Note: the new string keys (settings_item_language, language_system, language_english, language_korean) should be backfilled into values-ko once PR #602 lands. Co-authored-by: hy4ri <hy4ri@users.noreply.github.com> |
||
|
|
4314bddaae |
Add Korean translation (values-ko/strings.xml) (#602)
* Add Korean translation (values-ko/strings.xml) * Disable MissingTranslation lint check |
||
|
|
5376d46683 |
feat: queue prompts while the agent is running (#598)
Allow the chat input to be sent while the agent is mid-turn or awaiting
approval. The backend's prompt.submit busy-input policy already queues any
prompt that lands during a running turn (tui_gateway/server.py:_handle_busy_submit)
and drains it as the next turn, so the message is never dropped.
- ChatInputPolicy: pure, testable send/placeholder decision logic
- ChatScreen: canSend stays enabled while typing/approving; placeholder shows
'Queued — sends when agent is free' when text is present during a busy turn
- ChatInputPolicyTest: truth-table covering typing, approval-pending,
disconnected, empty, attachment, and slash-command cases
- strings: chat_input_placeholder_queue
Note: this supersedes the Genesi599 fork's approach (dispatchViaRpc('queue ...')),
which double-submits and relies on a 4018-prone command.dispatch path. The
backend-queued prompt.submit path is the correct, single-submission contract.
Co-authored-by: M57 <m57@hermes-control.app>
|
||
|
|
c543eb6470 |
feat(#530): server-backed session search (FTS5) (#555)
* 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> |
||
|
|
8d22988985 |
fix: shorten chat banner status text + prevent relogin button squish (#570)
* fix: shorten chat banner status text + prevent relogin button squish * fix: drop bad weight import (resolve internal RowColumnParentData conflict) --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
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> |
||
|
|
cc917afcbd |
fix(#548): stop System tab button text truncation + fix two wrong labels (#559)
* fix(#548): stop System tab button text truncation + fix two wrong labels * fix(system): remove inline icons from narrow system action buttons to prevent text cutoff * fix(system): restore action icons and implement dynamic font scaling for button text * fix: stack icon above text in system buttons to prevent label clipping Replace horizontal Icon+Text layout with vertical Column stacking (icon on top, text centered below) — the same pattern Material 3 NavigationBar items use. Gives text the full button width so labels like 'Security Audit' and 'Config Migrate' never get cut off. * fix: add missing ImageVector import for system buttons fix --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
9156064d2b |
feat(#537): analytics screen — usage + per-model stats (#558)
* feat(#537): analytics screen — usage + per-model stats from /api/analytics/* * fix(#537): address PR review — cancel-in-flight loads, retain cache on refresh, drop redundant async * fix(#537): surface real analytics failure, mirror backend shape (period_days/tools), round-trip test * fix(#537): tolerate float last_used_at in analytics (backend emits epoch as number) * fix(#537): type tools[] as AnalyticsToolUsage (tool/count/percentage), render Top tools section * fix(analytics): fix crashes and refactor tab layout * perf(analytics): cache responses and prefetch day windows --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
afa82ceec2 |
feat(#532): add process.list + process.kill manager screen (#556)
Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
53b3966931 |
feat(#525): render reasoning.delta / reasoning.available stream (#543)
* feat(#525): add ReasoningDelta + ReasoningAvailable WsEvent types * feat(#525): parse reasoning.delta + reasoning.available; route through reducer + StreamingState * feat(#525): add reasoning streaming buffer to ChatStreamingController * feat(#525): dispatch ReasoningDelta/ReasoningAvailable in ChatViewModel * feat(#525): render collapsible ReasoningIndicator in chat list * fix(#525): address review — icons+semantics, LocalHermesStatusColors, drop redundant ReasoningAvailable branch * feat(#525): persist reasoningText on model, entity, mapper + Room migration 3->4 * feat(#525): reducer + streaming carry reasoningText onto message * feat(#525): render persisted reasoning above each assistant bubble, drop global indicator * nix: flake * nix: flake --------- Co-authored-by: M57 <m57@hermes-control.app> |
||
|
|
d056c77c93 |
feat(chat): handle sudo.request / secret.request and respond via RPC (fixes #524) (#539)
Previously mobile dropped the gateway's sudo.request and secret.request events, so any agent turn needing a sudo password or a secret value hung forever with no UI. Now: - EventParser maps sudo.request / secret.request -> WsEvent.SudoRequest / SecretRequest - ChatViewModel surfaces a secure dialog (SudoPromptUi / SecretPromptUi) and replies via sudo.respond / secret.respond with session_id + request_id + payload - ChatWsEventReducer explicitly branches the new events (no-op, VM-owned) - ChatScreen renders password-masked SudoPromptDialog / SecretPromptDialog - 6 unit tests cover prompt-state, RPC params, and prompt-clearing Mirrors desktop/web contract (apps/shared GatewayEventName + tui_gateway). Derives from audit of /opt/hermes-agent desktop app. |
||
|
|
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) |
||
|
|
04bb7c4325 |
Fix/issue 470 stuck reconnecting (#495)
* fix(ws): stop chat opening from racing the singleton into a stuck RECONNECTING Root cause: HermesWsClient is a global singleton connected once at login; every tab observes its single connectionStatus. ChatViewModel is recreated on every chat-tab open and its init calls connectWebSocket() -> wsClient.connect(). connect() only guarded the CONNECTED case, so when the socket had dropped (singleton in RECONNECTING with its own scheduled reconnect) opening chat spawned a SECOND socket on the same webSocket field. The orphaned socket's later onClosed set connected=false + re-armed RECONNECTING after the new socket connected, and currentBackoff kept resetting on each chat open, leaving the status stuck on RECONNECTING while every other tab (which never calls connect()) stayed healthy. Fix: - HermesWsClient.connect() now no-ops while CONNECTING/RECONNECTING (in-flight socket already pending) and while AUTH_EXPIRED (needs re-auth, not a loop). - ChatViewModel.connectWebSocket() mirrors the same terminal-state guard so the chat tab never disturbs an already-working or already-recovering connection. Behavior-preserving: happy path (CONNECTED) and genuine terminal states (DISCONNECTED/NO_NETWORK) still connect as before. * fix(#470): sync cookie store scope with selected profile ID Update AuthManager to switch CookieManager to the active server profile ID on startup and on runtime profile change. * fix(#470): prevent stale token cache and load initial cookie scope Pass active profile scope to CookieManager.initialize on startup and update setProfileToken to sync the in-memory cachedToken. * feat(chat): add re-login action and credentials popup to connection banner Add a Re-login button to the connection status banner and a dialog to prompt for credentials, allowing the user to refresh session cookies/tokens and reconnect the WebSocket. * fix(chat): address PR #495 review feedback * fix(chat): restore reconnect action in banner * fix: resolve composable invocation in ReloginDialog * fix(chat): enable re-login action during reconnecting state Add the Re-login button to the connection status banner when connectionStatus is RECONNECTING, allowing users to re-authenticate and resolve stuck reconnect loops. |
||
|
|
739ff4f62b |
fix(chat): surface all disconnected states in reconnect banner (#491) (#493)
* fix(chat): surface all disconnected states in reconnect banner (issue #491) The connection-status banner only appeared for RECONNECTING/DISCONNECTED, so NO_NETWORK and AUTH_EXPIRED left the user staring at a silent chat with no feedback and no way to recover — the core of the 'reconnecting is not user-friendly' complaint. ChatTopBanner now shows for every non-connected, non-connecting state: - RECONNECTING: spinner + 'Reconnecting to Hermes…' (auto, no button) - DISCONNECTED: 'Disconnected from Hermes' + Reconnect - NO_NETWORK: 'No network connection' + Reconnect (force a retry) - AUTH_EXPIRED: 'Session expired' + 'Sign in again' -> AuthLoginScreen The auto-reconnect engine (backoff, network-return trigger, in-flight message queue in HermesWsClient) is untouched — this only makes the stuck states visible and actionable. * fix(chat): use LocalHermesStatusColors for reconnect banner (review fix) Per AGENTS.md, semantic status colors must come from LocalHermesStatusColors, never MaterialTheme.colorScheme. The reconnect banner used errorContainer/onErrorContainer directly, which is both a semantic-color violation and inconsistent with the rest of the app (Navigation.kt maps DISCONNECTED/NO_NETWORK/AUTH_EXPIRED -> .error; TelegramOnboardingDialog uses .errorContainer for error surfaces). Switch Surface bg to errorContainer and foreground (spinner, text, button content) to error via LocalHermesStatusColors.current. |
||
|
|
aa59e704a4 |
fix(#478): rework default login to use unified ConnectionProfile model (#482)
* 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> |
||
|
|
430a083647 |
feat(#471): implement DataStore-based server configuration and self-h… (#477)
* feat(#471): implement DataStore-based server configuration and self-healing * refactor(#471): address Sourcery feedback on ANR risk and reset method splitting * refactor(#471): implement synchronous ServerStore initialization to fully remove ANR risk * feat(#471): show profile name, host and port on add profile, redirect to login on save, and support shortcut login buttons for existing profiles |