Commit Graph
60 Commits
Author SHA1 Message Date
M57andM57 063c27b141 feat(settings): move language to its own section with scalable dropdown picker (#906)
- New Language row in the main Settings list with current-language summary
- New SettingsLanguage drill-down page with an OutlinedButton + DropdownMenu
  picker (scales to any number of languages; same pattern as theme presets)
- Shared supportedLanguages()/languageLabel() helpers — adding a language
  is now one entry in one place
- Removed the 4-option segmented pill row from the Appearance page
- Added settings_sec_language string (en + zh)

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-15 12:18:37 +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 daf33dcb4c feat(update): notify about app updates on launch (chat banner) (#891)
Fires one silent update check per installed version at app start
(UpdateNoticeManager, release builds only) and shows a non-blocking
banner on the chat screen when a newer release exists. The About tab
adopts the launch result instead of re-pinging GitHub; dismissals
return on the next launch via the persisted latest tag.

Closes #890

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 01:08:42 +03:00
M57andM57 ff8370bb8d fix(settings): About tab crash + update-row UI (#889)
- AppUpdateViewModel has no (Application) ctor; the default viewModel()
  factory's AndroidViewModelFactory does getConstructor(Application)
  -> NoSuchMethodException -> crash on opening About. Build it with an
  explicit viewModel { } initializer + APPLICATION_KEY instead.
- Add instrumented regression test that renders SettingsAboutPage with
  the real default factory (the exact path that crashed).
- UpdateRow: status text and action button no longer fight for one line;
  Update now / Open settings / Retry get a full-width button of their
  own below the row, Downloading gets a full-width progress bar.
- Strip the leading 'v' from GitHub release tags before formatting
  (templates already prepend 'v' -> was rendering 'vv1.21.1').

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-13 00:14:41 +03:00
M57andM57 683a24becf fix(profiles): connection-profile switch re-dials the WebSocket (#888)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-12 23:27:08 +03:00
M57andM57 345d2e4e89 feat(chat): remove legacy bubble renderer, full-bleed is the only chat style (#881)
Deletes the ChatMessageList/ChatStreaming bubble path and the ChatStyle
setting (issue #866 phase 6). ChatScreen now renders FullBleedChatList
directly; ChatBubble becomes the user-message anchor with the shared
SystemBubble/InlineAttachment/formatTimestamp helpers intact. Stubs of
AuthManager.getChatStyle() removed from four test suites; TodoTaskCardTest
renders ToolBubble directly.

Verified: ktlintCheck green, testDebugUnitTest 962/962 pass, assembleDebug
APK builds.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-12 11:02:33 +03:00
M57andM57 a59c8c6a4f feat(chat): full-bleed agent replies behind a chat style setting (issue #866) (#878)
* feat(chat): ChatStyle preference type + store field + AuthManager (issue #866)

* feat(settings): chat style toggle in Appearance (issue #866)

* feat(chat): plumb ChatStyle into ChatUiState + refreshSettings (issue #866)

* feat(chat): full-bleed turn grouping (issue #866)

* feat(chat): full-bleed agent message renderer (issue #866)

* feat(chat): full-bleed compact tool rows + system events (issue #866)

* feat(chat): full-bleed chat list renderer + streaming (issue #866)

* feat(chat): wire full-bleed renderer behind chat style setting (issue #866)

* test(theme): full-bleed contrast gate across all presets (issue #866)

Gate caught NORD light primary/background < 3:1 — header role label
switched to onSurfaceVariant (meta token, correct for header + passes
in every preset; NORD's pastel Frost accent can't reach 3:1 on light).

* test(chat): full-bleed renderer coverage (issue #866)

* style(chat): ktlint formatting pass on full-bleed renderer (issue #866)

* fix(chat): eagerly capture per-item state in full-bleed list (issue #866)

LazyColumn item{} lambdas run at composition time, so loop locals read
inside them saw final values: turn header never rendered (firstProseSeen
always true at compose) and milestones misindexed. Capture showTurnHeader
+ milestone as immutable vals during the DSL loop. Caught by fresh-context
review (decompiled Compose foundation to confirm item composition timing).

* test(chat): stub AuthManager.getChatStyle in mockkObject setups (issue #866)

refreshSettings()/loadSettings() now read AuthManager.getChatStyle();
the four test files that mockkObject(AuthManager) with explicit stubs
had no stub for the new method, so mockk fell through to the real
object and threw 'AuthManager not initialized'. Full suite: 953/953
passing, same shape as pristine origin/main.

* feat(settings): move chat style toggle from Appearance to Chat section

* feat(chat): hoist reasoning above tool rows in full-bleed turns

Tool rows stream in before the answer message, so the turn rendered
tools above the reasoning card. The turn's reasoning now renders at
the top of the agent turn, with tool rows and prose underneath.

Streaming is folded into the turn model (groupIntoTurnsWithStreaming)
so the hoist works live while the answer is still streaming, and the
detached streaming item is gone (which also fixed the duplicate
turn-header quirk for streaming continuations).

* feat(chat): cap reasoning card height + long-press to copy

Expanded trace caps at 40% of the screen with internal scroll (Show
full lifts the cap); long-press anywhere copies the reasoning.

* feat(chat): blinking typing caret while streaming

* feat(chat): make full-bleed the default chat style

* feat(chat): remove thinking dots, show agent header above reasoning

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-12 00:22:07 +03:00
M57andM57 f931565c0d feat(settings): in-app update from About tab (issue #867) (#875)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-08-10 20:02:42 +03:00
M57andM57 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>
2026-08-08 15:58:25 +03:00
AlexDev_ e39544ebd5 fix: keep transient ticket failures reconnecting (#740)
* fix: keep transient ticket failures reconnecting

* fix: recover rejected websocket sends

* fix: serialize websocket reconnect scheduling
2026-07-28 22:33:12 +03:00
M57 223f9b676c fix(settings): clarify active saved profile contrast and border (#708)
Fix active saved profile visibility in Catppuccin light mode and Monochrome light/dark modes by adjusting primaryContainer contrast and adding a primary border stroke.
2026-07-25 12:25:01 +03:00
M57 34ba93901b feat(theme): allow dynamic colors to override preset theme and disable preset selector when active (#684)
* feat(theme): allow dynamic colors to override non-default theme presets

* feat(settings): disable theme preset selector when dynamic colors is enabled

* feat(theme): align Catppuccin preset with official Latte and Mocha style guide
2026-07-22 22:49:36 +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
M57andM57 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>
2026-07-22 10:14:50 +03:00
M57andM57 e44a53366b Fix #647: resolve Default profile URL from top-level login URL (#649)
* Fix #647: resolve Default profile URL from top-level login URL

ConnectionProfile.resolvedBaseUrl (used directly by ConnectionSection for
the profile list display) silently fell back to the hardcoded loopback
127.0.0.1:9119 whenever a profile's own baseUrl was null, instead of using
the URL the user actually authenticated against.

- Add ConnectionProfile.resolveBaseUrl(topLevelBaseUrl) that prefers the
  profile's own baseUrl, then the top-level login URL, then legacy loopback.
- Store-level ServerCollectionOps.resolvedBaseUrl now applies the same
  fallback so a null-baseUrl profile still shows the real gateway.
- ConnectionSection, SettingsViewModel and ConnectViewModel resolve against
  AuthManager.getBaseUrl() so the loopback fallback can never surface.
- Add Issue647ProfileUrlTest covering login-stamp, null-baseUrl fallback,
  own-URL precedence, fresh-install loopback, and store-level resolution.
- Update existing AuthManagerTest assertions to the new property form.

* Fix #647 (root cause): cache login URL at top-level baseUrl

The previous change only made profile resolution prefer the top-level
login URL, but setBaseUrl() never actually wrote the top-level baseUrl
field — so it stayed DEFAULT_BASE_URL (https://127.0.0.1:9119/) forever.
A Default profile seeded with the loopback default (or whose own baseUrl
was never stamped) therefore still displayed the loopback URL after
logging in via a custom LAN address.

setBaseUrl() now also stamps the top-level baseUrl, making it the
authoritative "last successful login URL" cache. getBaseUrl() and the
store-level resolvedBaseUrl (profile-list display) now reflect the URL
actually authenticated against.

Add regression tests mirroring the exact AuthLoginViewModel.connect()
sequence (seed with loopback -> setBaseUrl(loginUrl) -> token) asserting
the Default profile, top-level baseUrl and store display all resolve to
the login URL, never loopback.

---------

Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 22:03:31 +03:00
M57andM57 88a068b204 Remove redundant logout button from Settings About tab (#641)
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
2026-07-20 17:43:49 +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
M57andHy4ri 0a0b612d3a refactor(#619): kill DRAWER_GESTURE_SCREENS — screen-owned drawer gestures via HermesScaffold (#624)
Replaces the fragile DRAWER_GESTURE_SCREENS global set + defensive
LaunchedEffect(snapTo(Closed)) + closeDrawer callback workaround with a
single source of truth: each screen declares its drawer-gesture preference
via HermesScaffold's new drawerGesturesEnabled parameter (default true).

Architecture:
- DrawerGestureController holds the active gesture state. HermesScaffold
  reconciles each screen's preference into it via SideEffect; when a screen
  opts out while the drawer is open, the controller closes the drawer itself.
- LocalDrawerGestureController CompositionLocal flows the controller from
  MainNavigation down to ModalNavigationDrawer, which reads .enabled and
  passes it to Material's gesturesEnabled parameter.
- DisableDrawerGestures() helper for entry screens that don't use
  HermesScaffold (Landing, AuthLogin, PairingCodeEntry).
- All 6 Settings drill-down sub-pages pass drawerGesturesEnabled = false.
- NavigationController.closeDrawer removed; navigateTo no longer needs a
  synchronous drawer-dismiss hook.

Removes: DRAWER_GESTURE_SCREENS set, both LaunchedEffect(gesturesEnabled)
and LaunchedEffect(drawerGesturesEnabled) blocks, drawerGesturesEnabled
local state, closeDrawer callback on NavigationController.

Fixes the recurring drawer-scrim stuck-open bug class (PRs #445, #454, #455).

Verified: ktlintCheck, assembleDebug, testDebugUnitTest all green locally.

Co-authored-by: Hy4ri <hy4ri@users.noreply.github.com>
2026-07-18 14:25:23 +03:00
M57andhy4ri 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>
2026-07-16 01:23:19 +03:00
M57andhy4ri 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>
2026-07-15 23:57:39 +03:00
M57 21092c79f2 refactor(settings): edit settings tab (#496)
* refactor(settings): remove current connection editor fields

Remove the active host, port, token fields, and clear token button from the main Connection tab, as profiles are now managed exclusively through the ProfileEditorDialog.

* feat(settings): migrate to full auto-save design and remove save button

Update settings change listeners (auto-reconnect, typing effect delay/toggle) to save state immediately to AuthManager. Remove the manual Save button from the scaffold's top bar and delete the redundant SaveIndicator.

* style(settings): clean up unused imports from settings UI cleanup

* fix(chat): resolve compose resource lint error and initial state test failure

Pre-resolve empty credentials string resource at the composable scope to fix LocalContext resource query lint error. Update testInitialStateAndConnection to use DISCONNECTED state to trigger connection and pass verification.
2026-07-08 02:07:11 +03:00
M57 ca32e37e2f refactor(ui): split monolithic screen files into focused components (#485)
* refactor(ui): extract SkillsStep to components package

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

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

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

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

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

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

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

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

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

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

* refactor(ui): extract PlatformCard + TelegramOnboardingDialog clusters

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

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

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

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

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

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

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

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

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

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

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

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

Map platform connection-status badges to semantic LocalHermesStatusColors
slots (success/warning/error) + add neutral slot for not_configured/disabled.
Removes hardcoded hex (Color(0xFF...)) and MaterialTheme.colorScheme usage
flagged by Sourcery; neutral defaults to StatusGrey so existing theme
providers are untouched.
2026-07-07 23:00:24 +03:00
M57 ce7048c07e Remove Settings profile dropdown; cards tappable to switch (#488)
* Remove Settings connection profile dropdown; make saved-profile cards tappable to switch

The profile dropdown in the Settings > Connection tab was the only way to
switch the active profile from Settings. Removing it left a regression, so
each saved-profile Card now has clickable { viewModel.selectProfile(profile.id) }
reusing the existing isActive highlight. The isNotEmpty guard stays so the
section hides cleanly when there are no profiles.

* Tighten spacing between Test Connection and Logout buttons (16dp -> 8dp)

* Tighten Test Connection / Logout spacing further (8dp -> 4dp)

* Tighten Test Connection / Logout spacing further (4dp -> 2dp)

* Add accessibility role to profile cards (clickable role = Role.Button)

Sourcery flagged the now-clickable profile Card as missing semantics for
assistive tech. Use clickable(role = Role.Button) so TalkBack announces it
as an actionable control. Role import added.
2026-07-07 21:37:14 +03:00
M57 be36bb24d7 feat: move logout button from About tab to Connection tab (#487)
- Remove the Logout button from the About tab content.
- Add the Logout button at the bottom of the Connection tab so users
  can sign out directly where they manage their connection.
- Delete the unused, duplicate AboutSection() composable (it was never
  called and carried its own Logout button) to keep a single source of
  truth for the action.

Logout behavior is unchanged: it still calls viewModel.logout() then
onLogout() (navigates to login). Verified with assembleDebug + ktlint.
2026-07-07 21:09:26 +03:00
M57andronia 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>
2026-07-07 20:50:53 +03:00
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
2026-07-06 22:40:44 +03:00
M57andHermes Agent d181851670 feat(#441): add profile cloning and description editing (#463)
* feat(#441): add profile cloning and description editing

* feat(#441): add profile builder wizard and API enhancements

Introduce 5-step Profile Builder wizard into ProfilesScreen and add support for create/clone endpoints.

* fix(#441): restore imports and add ProfileInfo constructor defaults

* style: run ktlint format on codebase to fix styling errors

* fix(#441): address PR review comments including provider slug bug, validation, and strings

* fix(#441): remove unused McpServerConfigInput import

---------

Co-authored-by: Hermes Agent <hermes@agent.local>
2026-07-03 22:24:42 +03:00
M57andM57 546dc86a98 fix(#424): fully fix logout→login cycle — clear ViewModel state + manage WS lifecycle (#429)
* fix(#424): fully fix logout→login cycle — clear ViewModel state + manage WS lifecycle

The previous fix (PR #427) fixed button responsiveness after logout
but left two issues that kept login-after-logout broken:

1. Stale ViewModel state — AuthLoginViewModel is Activity-scoped,
   so  stayed  from the first login. When
   the user returned to the login screen, LaunchedEffect fired
   instantly with the stale value and bounced them to ChatScreen
   without showing the form.

   Fix: DisposableEffect(onDispose) clears connectionSuccess,
   errorMessage, and isLoading when the screen leaves composition.

2. WebSocket lifecycle — logout() never disconnected the WS, so
   it stayed open with a null/stale token. After re-login, no one
   called connect() with the fresh credentials.

   Fix: HermesWsClient.disconnect() on logout,
   HermesWsClient.connect() after successful re-login.

* fix(#424): route landing screen clicks through navigateTo

Avoid direct backStack.add() calls to prevent duplicate screen overlay and unresponsive buttons.

* fix(#424): close drawer when gestures are disabled

Ensure the drawer is closed when transitioning to LandingScreen or LoginScreen to prevent the scrim overlay from intercepting clicks.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-30 17:16:17 +03:00
M57andM57 9344c3bd6e feat: add Neon Noir theme preset (#401)
Add a new NEON_NOIR theme preset with dark cyberpunk-pink aesthetic:
- Deep Void (#0f0b1a) background with Slay Pink (#ff6bf4) primary
- Full dark + light color schemes with proper contrast ratios
- Custom status colors matching the neon aesthetic
- UI dropdown entry and string resource

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-26 19:34:05 +03:00
M57andM57 2a9d4fa5be feat(#371): connection profiles full management (add, edit, delete) (#393)
* feat(#371): add full connection profiles CRUD (add/edit/delete)

* fix(#371): add missing TextButton import

* fix(#371): move Save button to header so it's visible on all settings tabs

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-26 08:07:30 +03:00
M57andM57 8d3d8a11e8 fix(#391): resolve 5 deprecation warnings (#392)
* fix(#391): resolve deprecation warnings

- Icon: Icons.Filled.ArrowForward → Icons.AutoMirrored.Filled.ArrowForward (KanbanScreen)
- Room: fallbackToDestructiveMigration() → fallbackToDestructiveMigration(false) (HermesDatabase)
- Tab: ScrollableTabRow → PrimaryScrollableTabRow (SettingsScreen)
- Clipboard: LocalClipboardManager → LocalClipboard + coroutine-scoped setText (ChatBubble: UserBubble, AssistantBubble, ToolBubble, CopyButton)
- ClickableText: pushStringAnnotation → pushLink(LinkAnnotation.Url), ClickableText → Text with native link handling (ChatBubble: RichText)

* fix(#392): replace deprecated setText with setClipEntry for new Clipboard API

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-26 05:25:01 +03:00
M57andM57 3399880621 perf(#281): fix PERF-13/14/15/16 — main-thread settings, 100Hz polling, leaked scope, WS disconnect (#359)
* perf(#281): fix PERF-13/14/15/16 — main-thread settings load, 100Hz polling, leaked scope, WS disconnect

* fix(#281): fix SettingsViewModelTest failures from async init dispatcher

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-25 20:39:52 +03:00
M57andM57 9297413b2a refactor(#285): extract section composables from SettingsScreen and ChatScreen (#358)
* refactor(#285): extract ConnectionSection from SettingsScreen

* refactor(#285): extract TestResultCard, SaveIndicator, SettingsActionButtons

* refactor(#285): extract BehaviorSection from SettingsScreen

* refactor(#285): extract AppearanceSection from SettingsScreen

* refactor(#285): extract ChatSection from SettingsScreen

* refactor(#285): extract NavBarSection from SettingsScreen

* refactor(#285): extract AboutSection from SettingsScreen

* refactor(#285): extract ChatLifecycleEffects from ChatScreen

* refactor(#285): extract ChatTopBanner from ChatScreen

* refactor(#285): extract ChatMessageList, ChatLoadingOverlay, ChatScrollToBottomFab

* fix(#285): use fully-qualified ButtonDefaults, BoxScope receiver for ChatScrollToBottomFab

* fix(#285): add BoxScope import

* fix(#285): pass isLoading as param instead of accessing StateFlow.value

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-25 20:36:21 +03:00
M57andM57 f190b67ee6 fix(#290): code health — magic colors, flag params, silent catch (#353)
* fix(#290): use LocalHermesStatusColors instead of magic colors (CODE-33)

* fix(#290): replace boolean-flag params with sealed NavIcon class (CODE-37)

* fix(#290): resolve compilation errors — Color import + nullable NavIcon params

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 21:26:32 +03:00
M57andM57 740b4fe601 Refactor: Consolidate navigation items into ScreenRegistry (#310)
* Refactor: Consolidate navigation items into ScreenRegistry

* fix(#310): remove unused imports, fix ktlint import ordering

* feat(#289): add ScreenRegistry for centralized screen configuration

* fix(#310): fix ktlint when-condition blank lines in Navigation.kt

* fix(#289): derive availableNavItems from ScreenRegistry

* fix(#310): use addEntryProvider instead of reified entry for dynamic screen registration

* fix(#310): remove double top padding from kanban, channels, achievements screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 18:19:38 +03:00
M57andM57 c1af989220 feat(#349): Smart auth login flow — probe dashboard and show correct fields (#351)
* feat(#349): smart auth login flow — probe dashboard, show correct fields

Landing screen with two buttons (Auth Login / Pairing Login) replaces
ConnectScreen as the start screen. Auth login probes the dashboard to
detect what auth it needs (token-only, basic-auth, or both) and shows
only the relevant input fields.

Changes:
- NavigationKeys.kt: add LandingScreen, AuthLoginScreen NavKeys
- LandingScreen.kt: new screen with hero icon + two login buttons
- AuthLoginScreen.kt: IP/Port input, probe logic, dynamic field rendering
- AuthLoginViewModel.kt: probe API, dashboard auth detection, connect flow
- Navigation.kt: wire new screens, start on LandingScreen, hide bottom bar
- strings.xml: add string resources for both screens

* fix(#351): align AuthLoginScreen field names with ViewModel API, fix TAG refs

- state.isProbing → state.probing
- DashboardAuthMode.BOTH → DashboardAuthMode.ALL
- viewModel.probe() → viewModel.probeDashboard()
- state.isConnecting → state.isLoading
- Removed unused TAG constant, replaced with inline tag string
- Made connectTokenOnly suspend to fix safeApiCall compilation

This fixes the CI compilation errors.

* fix(#351): revert probeDashboard() call to probe() to match VM

* fix(#351): rename isProbing to probing in ViewModel

* fix(#351): follow redirects in basic auth login — / redirects to /login

* fix(#351): show all fields when basic auth detected — still needs session token for WS

* refactor(#349): remove old ConnectScreen, clean up NavKey + navigation

* fix(#351): validate token against /api/sessions (auth-protected) instead of /api/status (public)

/api/status is a public endpoint that returns 200 even with wrong tokens,
causing invalid tokens to be saved and the WebSocket to spin forever on
the chat screen. /api/sessions requires auth and returns 401 on bad tokens.

* fix(#351): rewrite basic auth login — POST to /auth/password-login, then fetch SPA with cookie to extract token

* fix(#351): use ticket-based WS auth when basic auth is active

The updated dashboard on 0.0.0.0 always has auth_required=True, which
rejects legacy ?token= in favor of ?ticket= (single-use, 30s TTL).
Added AuthManager.setWsAuthParam() to support both ?token= (loopback)
and ?ticket= (gated mode) in wsUrl(). connectBasicAuth now mints a
ticket via POST /api/auth/ws-ticket after login instead of extracting
the token from the SPA.

* fix(#351): use requirePrefs() instead of direct prefs reference in wsUrl()

* fix(#351): handle relaxed-mock empty string in wsUrl() authParam fallback

* feat(#349): auto-extract session token from SPA HTML during probe

The probe now extracts the __HERMES_SESSION_TOKEN__ value from the
dashboard SPA and auto-populates the token field in TOKEN_ONLY mode,
eliminating the need for manual token pasting.

- Introduced ProbeResult data class to carry authMode + extractedToken
- Regex extraction of __HERMES_SESSION_TOKEN__ from dashboard HTML
- Token field auto-filled on probe completion for TOKEN_ONLY mode

* fix(#349): return BASIC_AUTH from probe when basic auth detected

Bug: probe returned ALL instead of BASIC_AUTH for basic-auth-only
dashboards, causing token+credential fields to show and the connect
flow to fall through to connectTokenOnly() on basic auth failure,
producing a confusing 'Unauthorized (HTTP 401)' NetworkResult error.

Fix: probe now returns DashboardAuthMode.BASIC_AUTH when a redirect
to /login is detected. BASIC_AUTH in connect() calls
connectBasicAuth() directly with no fallback. ALL mode also no longer
falls through to connectTokenOnly().

* fix(#349): use session cookie for REST API in gated mode

The dashboard's gated mode authenticates REST API requests via
session cookies (hermes_session_at), not Bearer tokens. The app
was storing the WS ticket as the token and sending it as
Authorization: Bearer <ticket> for all REST calls, which the
server rejected with 401.

- Extract hermes_session_at cookie from Set-Cookie header after
  POST /auth/password-login and store it in AuthManager
- ApiClient authInterceptor now sends Cookie header when session
  cookie is present, falls back to Bearer token for loopback mode
- ConnectResult carries both WS credential (ticket) and session
  cookie separately
- WS ticket used only for WebSocket (?ticket=), session cookie
  used for REST API

* feat(#349): add logout button to Settings > About

Red error-style button that clears all auth credentials (token,
session cookie, WS auth param) and navigates back to LandingScreen.
Wired through SettingsScreen onLogout callback to Navigation.kt.

* fix(#349): improve logout navigation flow

* fix(#349): keep backstack stable across logout

Use remember without keys for NavBackStack so the same backstack
object persists across startScreen changes. Use LaunchedEffect keyed
on startScreen to reset stack content atomically. This avoids
NavDisplay losing snapshot observation when a new NavBackStack
object is created.

* fix(#349): mint fresh WS ticket on every connect

The WS ticket is a single-use 30-second credential. On app restart
(or reconnect), the stored ticket is expired. Now refreshWsTicketIfNeeded()
mints a new ticket using the session cookie before every WS connection
attempt. Loopback mode (no session cookie) is unaffected.

* fix(#349): refresh ticket in openSocket for reconnect paths

scheduleReconnect() bypasses connect() and calls openSocket()
directly. Moving the ticket refresh into openSocket() ensures
every WS connection attempt — first launch and reconnect —
uses a fresh ticket.

* fix(#349): stub getSessionCookie() in ApiClientTest

mockkObject(AuthManager) is strict, so unstubbed getSessionCookie()
calls throw MockKException which manifests as IOException.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 16:37:09 +03:00
M57andM57 11ca9b55c8 feat: Reorganize settings into categorized tab pages (#345)
* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Adheres to ktlint conventions

* feat: Reorganize settings into categorized tab pages

Refactored `SettingsScreen.kt` to introduce a categorized tabbed interface using `ScrollableTabRow`, addressing issue #251. The monolithic settings scroll has been split into `Connection`, `Appearance`, `Behavior`, and `About` tabs to improve navigation and organization.

- Added `SettingsTab` enum to encapsulate tab states
- Grouped relevant `SectionCard` elements under respective tabs
- Nested Test/Save connection controls appropriately inside the connection section
- Cleaned up string resources for the newly created tabs
- Fixed UI bug by removing padding (`edgePadding = 0.dp`) in the TabRow to avoid excessive white space
- Adheres to ktlint conventions

* fix(#345): remove double padding on settings tabs

HermesScaffold already applies Scaffold's inner padding (top bar height +
status bar insets) via its own Box wrapper. The tabbed settings layout
was applying the same paddingValues a second time on the outer Column,
causing the tab row to sit ~64dp too low. Fix removes the redundant
.padding(paddingValues) call.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-24 11:26:10 +03:00
M57andM57 60bf6e8a33 feat: UI Changes
* fix(#220): implement edit soul and set model dialogs

* fix(#211): make UI more compact and information dense

* fix(#211): make navigation drawer sidebar more compact

* feat(#252): move settings item to end of navigation drawer

* feat(#250): add bottom navigation bar display mode settings

* feat(#225): add search bar filtering to remaining screens

* fix: make bottom navigation bar display modes more compact

* fix: resolve compilation error in KanbanScreen by referencing assignedTo

* feat: add search to achievements screen and top padding for icon-only navigation bar

* fix: adjust icon-only bottom bar height and remove padding

* fix(#249): remove unused PaddingValues imports in 6 screens

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-21 20:42:23 +03:00
Hy4ri 8e8fca756a feat(#208): add Monochrome, Gruvbox, Catppuccin and AMOLED theme presets 2026-06-21 13:01:55 +03:00
Hy4ri 78da6af6be feat(#232): dynamic light/dark theme switch without restart 2026-06-21 12:33:57 +03:00
Hy4ri cdad31b555 refactor(#78): extract UI strings for chat, settings, connect, kanban, and skills screens 2026-06-21 11:38:33 +03:00
Hy4ri 19d44c077b chore: format codebase with ktlint 2026-06-21 10:57:18 +03:00
M57 00c6e75f50 feat(connect): saved connection profiles support (#107) (#191)
* feat(#107): implement saved connection profiles support

* fix(connect): fallback profiles resolution and stubbing in tests
2026-06-21 00:18:35 +03:00
M57 64a047f095 feat: move session history to a dedicated history screen (#175)
* feat: move session history to a dedicated history screen

Closes #159.

* fix(ci): fix ktlint style issues and unresolved NavigationController reference
2026-06-20 22:27:54 +03:00
M57andM57 3c5fcf504c feat(#154): add word-by-word typing effect toggle and speed slider in settings (#165)
* feat(#154): add word-by-word typing effect toggle and speed slider in settings

* fix(settings): import Slider to fix build failure

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 17:09:42 +03:00
M57andM57 a2d299e136 feat(#129): remove redundant source archive, add About version card in Settings (#149)
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
2026-06-20 10:30:18 +03:00
M57 46b24593f9 feat(#58): implement skill markdown/yaml editor UI (#121) 2026-06-19 23:58:34 +03:00
Hy4ri 8755338636 Revert "Merge pull request #42 from Hy4ri/feat/sessions-screen"
This reverts commit 7d50b5acd7, reversing
changes made to ee97be50cb.
2026-06-19 23:18:26 +03:00
M57 7d50b5acd7 Merge pull request #42 from Hy4ri/feat/sessions-screen
feat: dedicated Sessions browser screen (Issue #40)
2026-06-19 23:14:40 +03:00
Hy4ri 508aea8bfa fix(#79): centralize retrofit error handling with NetworkResult 2026-06-19 22:58:33 +03:00