Commit Graph
102 Commits
Author SHA1 Message Date
M57andM57 cfe3e736fd chore: upgrade ktlint to 1.8.0 and format codebase (#1024)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-31 12:27:40 +03:00
M57 ad838f696e chore: add missing imports and fix test closing brace 2026-08-31 11:24:45 +03:00
Hy4ri d8e3992c33 fix: memory screen reset actions serialization error (fixes #1020) 2026-08-31 11:10:26 +03:00
M57 ece0288ca8 feat(update): surface backend update receipt in action popup (#958) (#964)
Adopt GET /api/hermes/update/receipt and append its summary to the
end of the hermes-update action progress popup after it finishes.
Additive only: existing checkHermesUpdate + connection-retry polling
is untouched. 404 (old backend) is handled gracefully.

- add UpdateReceiptResponse data model matching backend schema
- add getUpdateReceipt() to HermesApiService
- ActionProgressController gains trailingLines + pushTrailingLines
- ActionProgressDialog renders trailingLines after the log
- SystemViewModel fetches receipt on action finish and formats it
- extract pure formatUpdateReceiptLines for mockk-free testing
2026-08-24 01:35:59 +03:00
M57andM57 764cb3455f fix(auth): await cookie scope hydration on cold start (#961) (#962)
* fix(auth): await cookie scope hydration before ticket mint and request loading (#961)

* refactor(auth): scope latch activation to active store hydration

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-23 23:57:37 +03:00
M57andHy4ri 59bcbdfe3c build: Android 17 (API 37) + okhttp 5.5.0 + LAN local-network permission gate (#941)
* build: compileSdk 36 -> 37 + okhttp 5.4.0 -> 5.5.0

Take okhttp 5.5.0 (folds in dependabot PR #940) which hard-requires
compileSdk 37 via AAR metadata. targetSdk stays 36 for now.

* build: targetSdk 36 -> 37 (Android 17) + ACCESS_LOCAL_NETWORK gating

Opt into Android 17 runtime behavior. The app connects to a gateway
(default 127.0.0.1:9119, plus LAN + remote), so targeting 37 forces the
new ACCESS_LOCAL_NETWORK permission for LAN hosts.

- AndroidManifest: declare ACCESS_LOCAL_NETWORK (harmless below API 37)
- LocalNetwork.kt: pure detector + needsLocalNetworkPermission(baseUrl)
  (loopback + public remote exempt; RFC1918/link-local/CGNAT gated)
- ConnectViewModel: requestConnect() gates on the permission; screen
  launches the prompt; onLanPermissionResult proceeds or shows a clear error
- flake.nix: android platform 36 -> 37
- Tests: LocalNetworkTest + ConnectPermissionGateTest (SDK_INT / permission
  branching mocked, no device needed)
- Task 8 finding: assistant services (VoiceInteraction*/Recognition) and the
  remoteMessaging ChatNotificationService do NOT hit the Android 17 mic/
  while-in-use foreground hardening, so no change required.

* fix: ktlint + @Composable LocalContext hoist (was blocking CI)

- ConnectScreen: hoist LocalContext.current out of onClick lambda
  (calling a @Composable inside a non-composable lambda broke compileDebugKotlin)
- ktlint --format: multiline-expression-wrapping, function-signature,
  no-empty-first-line-in-class-body, no-unused-imports
- LocalNetwork.kt: merge consecutive KDocs (no-consecutive-comments)

flake.nix buildToolsVersion/platformToolsVersion intentionally stay 36.0.0
— they are decoupled from compileSdk; only platformVersions needs 37.

* fix: suspend mock (coEvery) + flake toolchain pinned to 37.0.0

- ConnectPermissionGateTest: getStatus() is suspend → use coEvery (was
  breaking compileDebugUnitTestKotlin)
- flake.nix: buildToolsVersion/platformToolsVersion/cmdLineToolsVersion
  pinned to 37.0.0 (verified against pinned nixpkgs 241313f — resolves to
  android-sdk-*-37.0.0 derivations; platformVersions already 37)

* fix(flake): revert cmdLineToolsVersion to 11.0 (not API-aligned)

cmdline-tools uses its own versioning (latest = 21.0, never 37.0.0) —
nixpkgs rejects 37.0.0 with 'version missing'. build-tools + platform-tools
stay 37.0.0 (verified resolve to android-sdk-*-37.0.0). flake now evals
clean against pinned nixpkgs 241313f.

* flake: track nixos-unstable + pin toolchain to latest (cmdline 22.0, platform-tools 37.0.1, build-tools 37.0.0, platform 37.1)

- inputs.nixpkgs -> nixos-unstable, flake.lock bumped to rev ffb3c9b7 (2026-08-19)
- cmdLineToolsVersion 11.0 -> 22.0 (own versioning, not API-aligned; v22_0 confirms)
- platformToolsVersion 37.0.0 -> 37.0.1
- buildToolsVersion stays 37.0.0
- platformVersions 37 -> 37.1
- verified: nix flake check --no-build -> all checks passed on hyari

* fix: unit tests — Uri stub + Build.SDK_INT final-field mock

- LocalNetwork.kt: needsLocalNetworkPermission uses java.net.URI instead of
  android.net.Uri (latter is a non-functional stub in JVM unit tests, its host
  returns null -> gate always false). Added pure requiresLocalNetworkPermission(sdkInt, baseUrl).
- ConnectViewModel: inject sdkVersion (package-private) instead of reading the
  final static Build.VERSION.SDK_INT directly; gate via requiresLocalNetworkPermission.
- ConnectPermissionGateTest: drop mockkStatic(Build.VERSION) (MockKException on
  final static field, and against repo convention); inject sdkVersion; add pure
  gate-decision matrix. Fixes 7 red tests.

* fix(test): remove illegal ':' from test function names (compile error)

* fix(test): order getString mocks so specific resources win over wildcard

* fix(flake): include android-37.0 system image so the emulator can boot

Google only publishes the android-37.0 system image (no 37.1 image exists
yet). With platformVersions=["37.1"] composeAndroidPackages found no matching
system image, leaving system-images/ empty and every AVD FATALing with
'Broken AVD system path'. Adding 37.0 pulls the published image so an AVD boots
(verified: hermes37 AVD boots Android 17 / API 37 on hyari). compileSdk/targetSdk
stay 37.

Also bump AGENTS.md SDK note 36 -> 37 (was stale).

* fix(flake): platformVersions 37.1 -> 37.0

Drop 37.1; 37.0 provides both the SDK platform for compileSdk 37 and the
published android-37.0 system image the emulator needs.

* fix(flake): remove stray tool-scratch line leaked into flake.nix (line 116)

A terminal tool's /tmp scratch rename line got appended to flake.nix, making
it invalid Nix. Removed; flake now parses + 'nix flake check' passes on hyari.

* fix(build): pin buildToolsVersion 37.0.0 to match nix SDK

AGP defaulted build-tools to 36.0.0, which isn't in the nix-managed SDK
(only 37.0.0 is provisioned). Local ./gradlew builds then tried to *install*
36.0.0 into the immutable /nix/store and failed. Pin 37.0.0 so the local
nix devShell builds. CI uses its own SDK so unaffected.

---------

Co-authored-by: Hy4ri <Hy4ri@users.noreply.github.com>
2026-08-20 14:35:42 +03:00
M57andM57 50a0f30d8f feat(#920): adopt GET /api/files/stream for seekable audio and video playback (#930)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-18 12:02:36 +03:00
M57andM57 d6892cde7f fix(#918): fix profile clone endpoint and prune dead MCP restart & session prompt REST calls (#926)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-17 22:51:29 +03:00
M57andM57 d8be45b8fc fix: reuse cached gateway downloads within a 10-minute TTL (#915)
Every tap on a gateway attachment re-downloaded the whole file: the
cache write used a random UUID prefix, so no two fetches ever produced
the same filename and old copies were never looked up.

Cache files are now named deterministically from the gateway path
(UUID3 hash + filename), so repeat opens within the TTL (10 min) reuse
the existing download and skip the network entirely. Stale entries fall
through to a fresh fetch that overwrites the cache via temp-file +
rename, so a failed re-download never destroys a previously-good copy.
Content types are remembered in-memory per path so cache hits still
hand the viewer the right MIME.

Also adds isFreshCacheFile()/cacheFileNameFor() unit tests.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 23:05:10 +03:00
M57andM57 b653903fa8 fix: stream gateway downloads to disk instead of slurping into RAM (#914)
* fix: stream gateway downloads to disk instead of slurping into RAM

Opening or saving a ~100MB MEDIA file OOMs the app: fetch() read the
whole response body into a single ByteArray (peak heap ~2x file size)
before writing it to cache. A 102MB docx (~97MiB) passed the server's
100MiB cap but blew the phone heap (192MiB growth limit) on both open
and save.

fetch() now streams the body to a cache file in 64KiB chunks and
GatewayFile carries the written file instead of bytes; save copies the
cache file to the destination chunked. Cancellation-safe (partial file
deleted, CancellationException never swallowed), UUID-named cache
files, 24h sweep.

Closes #913

* fix: stub app.cacheDir/applicationContext in attachment tests

The streaming fetch builds File(cacheDir, ...) from the application
context's cacheDir; the affected tests never stubbed it, so the mock
returned a bogus File (null path) and java.io.File's constructor NPE'd
before fetch was even called. Stub cacheDir + applicationContext
(returning app itself, matching the existing success-path test) in the
five open/save tests. copyChunked is also stubbed to copy for real in
the save-writes test since mockkObject mocks the whole object.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 23:05:10 +03:00
gslzwhlandhermes-i18n fdbd98af3e Add Simplified Chinese (zh) localization (#901)
* feat: add Simplified Chinese (zh) localization

- Add complete values-zh/strings.xml (921 strings, all keys covered)
- Translate 107 hardcoded UI strings across 34 Kotlin files
- Add 简体中文 option to in-app language picker
- Terminology aligned with official Hermes dashboard zh translations

* fix: resource-ize hardcoded Chinese strings per review

Address Hy4ri's review on PR #901:
- Move 104 hardcoded Chinese UI strings to string resources
  (English in values/strings.xml, Chinese in values-zh/strings.xml)
  referenced via stringResource()
- SystemViewModel labels -> @StringRes + AndroidViewModel with
  resource-based toast templates
- Make TodoTaskCardTest assertion locale-agnostic via getString()
- Update cookie tests to assert new host-agnostic session cookie
  behavior (WiFi/Tailscale switch-safe)
- Session cookie mirroring to WILDCARD bucket survives host switches

* style: apply ktlintFormat per review

---------

Co-authored-by: hermes-i18n <hermes-i18n@localhost>
2026-08-15 11:33:43 +03:00
M57andM57 90c1e8dc5f fix: tab reloads actually reload — kanban switch spam, plugins/skills refresh, ws mint (#897)
- ws: ticket-mint response body was read on the calling (main) thread —
  the socket read threw NetworkOnMainThreadException, the mint failed,
  and the kanban events stream never connected (board stuck Offline).
  execute() + body read now run entirely on Dispatchers.IO; same fix in
  DashboardSessionTokenRefresher.fetch (loopback mint path).
- kanban: re-selecting the already-active board reloads data and
  reconnects events WITHOUT the redundant POST /boards/{id}/switch that
  refresh fired on every tap.
- plugins: top-bar Refresh + pull-to-refresh wired through HermesScaffold
  (previously no reload control at all).
- skills: Refresh icon now does a plain GET reload; the heavy hub-update
  POST moved to the cloud icon so refresh is no longer disguised as update.
- test: mock the 3-arg Log.w overload (catch now logs the full stack).

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 14:27:36 +03:00
M57andM57 685b870cad feat(history): pin sessions to the top of the history tab (#886)
- Hold-menu Pin/Unpin action on session cards (PATCH /api/sessions/{id}
  with {pinned}, verified against the backend SessionRename model)
- Pinned sessions sort to the top on every load (client-side stable sort
  — the backend back-fills pins into page 1 but does not lift them)
- Pin indicator on pinned cards; search-result cards omit the pin action
- Tests: pin-first sort on load, toggle success (moves to top), failure
  (order kept + toast)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-12 14:32:48 +03:00
M57andM57 4a034684bb feat(chat): adopt order=latest for transcript initial load and paging (issue #859) (#870)
Backend now supports order=latest on GET /api/sessions/{id}/messages
(offset measured back from the newest message, page returned
chronologically). The mobile initial load previously computed its anchor
from a possibly-stale session-list message_count, which landed the first
page at the wrong position (dropped history or a duplicated tail).

- HermesApiService.getSessionMessages: add @Query("order")
- Initial load requests the newest page directly (order=latest, offset=0)
  and drops the fetchServerMessageCount offset math; legacy backends
  (no pagination echo) fall back to the count-based anchor
- Backward paging advances the from-end offset by a full page; a short
  page marks the oldest boundary and stops pagination
- ChatMessage stable keys under latest paging use the server's
  AUTOINCREMENT row id (from-end positions shift as the transcript
  grows and would collide across hydrations)
- syncCurrentSession refetches the newest page and merges by row id
  with a logical-content fallback for re-keyed rows

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-10 14:40:20 +03:00
M57andM57 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>
2026-08-07 21:36:23 +03:00
M57andM57 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>
2026-08-07 16:44:41 +03:00
M57andM57 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>
2026-08-07 14:20:21 +03:00
M57andM57 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>
2026-08-07 04:18:50 +03:00
M57andM57 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>
2026-08-06 17:57:58 +03:00
M57andM57 d9f437939a test(connect): phase 5 — restart lands on the selected profile, no login (#827)
* test(connect): pin restart-with-scoped-profile token resolution (phase 5)

* fix(profiles): scope switch via un-clamped active-profile state

The switch persisted the server profile through serverStore.selectedProfileId,
whose selfHealed() clamps ids absent from the local connection list — server
profiles like "meow" never exist there, so the scope silently reset to
default and the re-dialed socket created the fresh session in the wrong
profile (live logcat 2026-08-06: session.create carried profile=default
right after switching to meow).

- AuthManager: prefs-backed activeProfileId state, immune to the clamp,
  restored on startup; resetAuthStateForTest clears it
- ProfileScopeInterceptor + WsProfileParams + ChatViewModel fetchContextUsage:
  read the active profile instead of the connection selection
- ProfileSwitchCoordinator: write activeProfileId; dial the socket on
  Dispatchers.IO (the ticket mint did blocking I/O on the main thread —
  NetworkOnMainThreadException — and fell back to a 1s reconnect)
- ProfilesViewModel: inject the IO dispatcher; ProfilesViewModelTest drops
  its static Dispatchers mock (JVM-wide bleed class)
- Tests: WsProfileParams/Interceptor tests use the real prefs-backed state
  instead of mockkObject(AuthManager) (re-mock breaks in suite order after
  real AuthManager init); AuthManagerTest + Issue647ProfileUrlTest cancel
  the init() DataStore scope in tearDown (UncaughtExceptionsBeforeTest
  phantoms); regression test pins the clamp on the old path vs the new state

* fix(profiles): resolve getter/property JVM collision; de-poison slash test

- AuthManager.activeProfileId (StateFlow) and a getActiveProfileId()
  function generated the same JVM getter name — calls silently resolved to
  the property getter at runtime (ReadonlyStateFlow -> String
  ClassCastException, CI unit red 2026-08-06). Function removed; callers
  read activeProfileId.value.
- ChatViewModel: inject ioDispatcher (38 sites) so SlashCommandDispatchRpcTest
  can drop its mockkStatic(Dispatchers) bleed without racing real IO.
- SlashCommandDispatchRpcTest: drop the static Dispatchers mock (JVM-wide
  bleed; CI cross-wired COMMAND_DISPATCH captures in suite order), construct
  the VM with the test dispatcher.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-06 15:07:35 +03:00
M57andM57 75ac0c4790 feat(profiles): first-class profile switching — phase 0 baseline port (#820)
* feat(profiles): make profile switching actually scope the app (issue #781 follow-up)

* fix(profiles): inherit connection token when switching to same-dashboard profile (issue #781 follow-up)

* fix(chat): reset open session on profile switch + prefill login profile (desktop parity)

* fix(ws): inject profile into profile-scoped RPC params so chat follows the selected profile

* test(chat): de-poison SlashCommandDispatchRpcTest (drop static Dispatchers mock)

mockkStatic(Dispatchers::class) + a stubbed Dispatchers.IO bleeds JVM-wide and
breaks later classes in suite order (CI 2026-08-06: COMMAND_DISPATCH captures
cross-wired → ComparisonFailure on lower stack PRs). ChatViewModel gains an
injected ioDispatcher (38 sites); the slash test constructs the VM with the
test dispatcher so the IO stub is unnecessary.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-06 15:06:28 +03:00
M57andM57 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>
2026-08-05 22:44:13 +03:00
M57andM57 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>
2026-08-05 17:59:30 +03:00
M57 e1a04a9396 ⚡ Optimize PersistentCookieJar by removing runBlocking (#799)
* perf: remove runBlocking from PersistentCookieJar to fix overhead

* perf: remove runBlocking from PersistentCookieJar to fix overhead

* perf: fix PersistentCookieJar race condition using CountDownLatch

* perf: remove runBlocking from PersistentCookieJar to fix overhead
2026-08-04 19:43:32 +03:00
AlexDev_ d1dac57ccb fix(ws): reconnect after network handover (#808) 2026-08-04 18:09:22 +03:00
M57andM57 1870b375b1 feat(skills): hub sources + featured landing and security scan (#779) (#793)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-03 22:13:28 +03:00
M57andM57 00a0eedc77 feat(logs): server-side file/level/component/line filters (#777) (#792)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-03 20:28:30 +03:00
M57andM57 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>
2026-08-03 19:49:31 +03:00
M57andM57 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>
2026-08-03 18:19:08 +03:00
M57andM57 37900a9af3 fix(#774): use GET for plugin rescan to match backend (#788)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-03 10:25:23 +03:00
M57andM57 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>
2026-07-31 05:30:51 +03:00
M57andM57 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>
2026-07-29 21:43:08 +03:00
M57 5c1492a80e feat(chat): add animated GIF rendering with tap-to-play animation (#721) (#731)
* feat(chat): add animated GIF rendering with tap-to-play animation

Closes #721

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

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

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

* fix(chat): control GIF playback via Animatable start and stop
2026-07-27 18:32:45 +03:00
M57andM57 a1155e8015 feat(#724): full gateway-file support — attach MEDIA: files (images, audio, video, docs) (#725)
* fix(#724): inline host-path MEDIA: images on mobile (client-side)

The previous approach re-fetched the REST session-messages endpoint, but that
endpoint returns the raw MEDIA:<path> text (api_server._message_response does
not inline, and the transcript stores raw content). The desktop app instead
reads the file locally via readFileDataUrl because it is co-located with the
host. Mirror that on mobile: on assistant message.complete/done, inline any
MEDIA:<path> image into a base64 data:image/... markdown tag the renderer can
show. Mobile-only, backend untouched. Unreachable tags are stripped (graceful
degrade). Pure inlining logic extracted to MediaInline for unit testing.

* fix: correct RegexOption name (IGNORE_CASE) in MediaInline

* test: fix byte literal types in MediaInlineTest

* fix(#724): rewrite host-path MEDIA: to gateway /api/files/download URL

Pivot from the earlier base64-inline approach to the desktop-proven,
remote-phone-working fix: rewrite MEDIA:<path> directives (delivered raw over
the WS stream) into authenticated
![image](<baseUrl>/api/files/download?path=<enc>&token=<enc>) markdown tags,
mirroring desktop media.ts mediaExternalUrl. The mobile renderer (MarkdownText)
shows http(s) ![]() via Coil, so agent-sent images now render on mobile too —
including on a remote phone (real HTTP, unlike a host-local file read).
Mobile-only, backend untouched. Pure rewrite logic in MediaUrlRewriter
(replaces MediaInline) with unit tests.

* feat(#724): full gateway-file support — attach MEDIA: files (not just images)

Pivot the MEDIA: fix from image-only URL rewriting to real attachments:
- Attachment: add gatewayUrl + AttachmentSource (LOCAL/GATEWAY) so
  gateway-hosted files are first-class and render via existing InlineAttachment.
- HostMediaExtractor (replaces MediaUrlRewriter): parse ANY absolute MEDIA:
  directive (any extension) and strip them from message text.
- ChatViewModel.attachHostMedia: build the authed /api/files/download URL via
  GatewayFileClient, classify via MediaTypes, attach as Attachment (images
  inline, every other file tappable). Idempotent.
- InlineAttachment: tappable. Gateway files fetch via GatewayFileClient and
  share via a chooser Intent (FileProvider); images load the URL in Coil.
- Hence agent-delivered images, audio, video, CSV, PDF and arbitrary files all
  work on mobile — including a remote phone. Backend untouched.

Builds on #726 (GatewayFileClient). Rebased onto main after #726 merged.

* fix(#724): resolve imports, bubble callback scope, and quoted media path regex

* feat(chat): open attachment in-place on tap (#724)

Tap opens LOCAL (content://) and GATEWAY (fetch->FileProvider) files via
ACTION_VIEW; failures surface as a non-blocking snackbar via openError.

* fix: remove redundant out-of-order io.mockk.* wildcard import (ktlint CI)

ktlint import-ordering requires lexicographic order; the trailing wildcard
'import io.mockk.*' sorts before 'io.mockk.coEvery' and was redundant — every
mockk symbol is already explicitly imported, and the single mockkConstructor
call uses a fully-qualified io.mockk. prefix (needs no import).

Fixes ktlint failure on PR #725.

* fix(ci): mock android.jar Intent stub so openAttachment GATEWAY test passes

android.jar unit-test stubs Intent.setDataAndType/addFlags to throw
'... not mocked'. openWithView builds an Intent and calls them, so
openBytes' runCatching swallowed the exception and startActivity never
fired, failing the test. Mock the Intent constructor + setters and
capture the constructed instance to assert the ACTION_VIEW contract.

* fix(#724): resolve host media download URLs and deduplicate REST synced chat bubbles

* fix(#724): add FLAG_ACTIVITY_NEW_TASK for file intents and update GatewayFileClientTest

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-27 14:52:49 +03:00
M57andM57 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>
2026-07-26 22:46:36 +03:00
M57andM57 439ae9fcba feat: gateway files client — fetch any gateway-hosted file over HTTP (#726)
* feat: gateway files client — fetch any gateway-hosted file over HTTP

Add GatewayFileClient: a typed client for the gateway's managed-files
download endpoint (GET /api/files/download?path=<enc>&token=<enc>), which
streams the raw bytes of any file living on the gateway host. This is the
mobile equivalent of desktop's mediaExternalUrl() and works on a remote
phone (real HTTP, not a host-local file read).

- buildDownloadUrl(): pure authed-URL builder (null on blank creds /
  non-absolute path), mirrors desktop's token-in-query auth.
- fetch(): OkHttp GET via the shared client, maps 401/403/404/413 to typed
  GatewayFileResult variants; returns GatewayFile(name, mime, bytes).
- MediaTypes: IMAGE/AUDIO/VIDEO/FILE classification by extension (mirrors
  desktop MEDIA_BY_EXT) so future consumers can present images, audio,
  video, or generic files — beyond just images.

Pure builders/classifiers are unit-tested. Mobile-only, backend untouched.
This is the foundation PR; issue #724's MEDIA: rendering will consume it.

* fix: decode filename + handle download-URL in mediaNameFromPath

- GatewayFileClient.parseFilename now URL-decodes the captured value so
  filename*=UTF-8''a%20b.png yields 'a b.png'.
- MediaTypes.mediaNameFromPath extracts the filename from the percent-encoded
  path= query param of a full /api/files/download?path=<enc> URL (the real
  case), not just a bare path.

* fix(remote): fix unit tests for gateway file client and media parsing

Replace plus-encoded spaces with %20 in download URL builder, URL-decode filenames in parseFilename, and extract target filename from URL path parameters.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-26 21:02:26 +03:00
M57andM57 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>
2026-07-26 01:52:22 +03:00
M57andM57 8c13f9bed0 perf: optimize synchronous I/O in AuthLoginViewModel (#699)
* perf: optimize synchronous I/O in AuthLoginViewModel

Replace blocking OkHttp .execute() with a suspending .await() extension
(CallExt.kt) backed by suspendCancellableCoroutine, freeing Dispatchers.IO
threads during network waits. Probe/connect paths made suspend.

- Call.await() guards both onResponse/onFailure with continuation.isActive
  to avoid AlreadyResume on cancellation.
- Removes non-asserting AuthLoginViewModelBenchmark timing dump.

* perf: optimize synchronous I/O in AuthLoginViewModel

Replaced the blocking `.execute()` OkHttp calls with a suspending `.await()` extension function inside `AuthLoginViewModel`. This frees up the `Dispatchers.IO` threads by deferring the blocking operations to OkHttp's internal task runner, providing a safer async approach and preventing Thread pool starvation on slow network responses.

Modified `probeDashboardInternal` and `connectBasicAuth` to be proper `suspend` functions and resolved corresponding unit tests.

* fix(test): add missing import io.mockk.any in AuthLoginViewModelTest

* fix(test): fix mockk imports and Log matchers in AuthLoginViewModelTest

* fix(test): use persistent mockProbeClient in AuthLoginViewModelTest

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-23 22:23:53 +03:00
M57andM57 152b7d5c0d fix(#700): omit Bearer header on gated-mode REST calls (#701)
In gated mode (basic-auth cookie flow) the dashboard 401s any request that
carries an Authorization: Bearer header alongside the valid session cookie
(verified live 2026-07-23). ApiClient.authInterceptor stamped the WS ticket
as a Bearer header on every REST call, so every non-chat tab (skills, cron,
config, tools, mcp, ...) failed with "token expired" while the WS chat
(?ticket=) kept working.

- Add AuthManager.isGatedMode() (wsAuthParam == "ticket").
- authInterceptor skips the Bearer header in gated mode; loopback/token
  behavior unchanged.
- TokenRefreshAuthenticator bails in gated mode instead of attempting the
  loopback-only SPA token refresh.
- ApiClientTest: add gated/loopback regression tests. 7/7 pass, ktlint clean.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-23 14:46:36 +03:00
M57 7927944198 Revert "fix(#688): route expired auth sessions to sign-in (#689)" (#698)
This reverts commit a5baf41545.
2026-07-23 12:40:29 +03:00
M57 a5baf41545 fix(#688): route expired auth sessions to sign-in (#689)
* fix(#688): route expired auth sessions to sign-in

Adopt global 401 latch, WS disconnect, and Back/profile guards.

* chore: clean up formatting noise

* fix(ktlint): reorder imports to place java and kotlinx at end

* fix(ktlint): place java.io.IOException at end of imports

* test: fix NavBackStack type parameter in AuthenticationRoutingTest

* test: add AuthSessionState tests to AuthLoginViewModelTest

* fix(ktlint): fix import order in AuthLoginViewModelTest
2026-07-23 00:44:17 +03:00
M57andM57 def122ab58 fix(#686): implement robust history pagination (#687)
* fix(#686): implement robust history pagination

Honor server-returned offset, degrade gracefully on older servers, and parse JSON tool results.

* fix(ci): repair unit-test compilation and pagination test mocks

- Remove trailing blank line in SessionMessagesResponse.kt (ktlint)
- Replace unresolved io.mockk.eq/coAnswers usage in ChatViewModelTest with
  an ordered-call counter inside coAnswers, which compiles and runs on
  MockK 1.14.11 (eq is a matcher-scope member, not a top-level import)
- Align HermesApiServiceMockWebServerTest with the PR's JsonElement content
  model (contentText) and the new include_compacted=true default query param

All unit tests pass locally with the ByteBuddy agent preload.

---------

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

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

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

Verified locally: compileDebugUnitTestKotlin + 48 tests pass.

* fix: remove unused imports flagged by ktlint

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

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-07-22 15:51:07 +03:00
M57 f7afbfae85 perf(model): optimize model tab load speed with two-phase fetch and loadQuick (#677) 2026-07-22 14:46:38 +03:00
M57andM57 e12a23befe fix: correct bulk session delete endpoint + surface deleted count (#653)
* 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>
2026-07-21 12:43:06 +03:00
M57andM57 a62bc7e16c refactor: remove device Pairing feature entirely (issue #645) (#650)
* refactor: remove device Pairing feature entirely (issue #645)

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

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

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

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 23:23:29 +03:00
M57andM57 c8a5055a84 fix(auth): support __Host-/__Secure- prefixed session cookies and proxy-relative paths (issue #635) (#648)
- CookieSerialization: add SESSION_COOKIE_NAMES (__Host-, __Secure-, bare) and
  isSessionCookieName(); isSessionCookie now matches any variant.
- PersistentCookieJar.getSessionCookieValue(): iterate SESSION_COOKIE_NAMES,
  returning the strictest prefixed match when present.
- HermesWsClient.refreshWsTicketIfNeeded(): drop the bare-name session-cookie
  preflight in gated mode — the server may hold a valid refresh cookie under a
  prefixed name; the authenticated ticket endpoint is the authority. Ticket URL
  already derived from endpoint (issue #634).
- HermesWsClient: stop logging WS close/failure reason strings and exception
  messages (class name only) to avoid leaking server context / URLs.
- ProfileScopeInterceptor: match profile-scoped paths via endpoint-relative
  request path so reverse-proxy prefixes (e.g. /hermes/api/...) are honored;
  wrap endpoint read in runCatching.

Verified: ktlintCheck, testDebugUnitTest, lintDebug all green locally.

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 20:56:39 +03:00
M57andhy4ri 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>
2026-07-20 17:26:10 +03:00
M57 d1fc477bcd fix: resolve all build compilation and style check warnings (#597)
* fix: resolve all build compilation and style check warnings

* fix: remove unused import in SystemViewModel
2026-07-14 21:55:37 +03:00
M57andM57 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>
2026-07-14 12:47:59 +03:00