* perf(model): optimize model tab load speed with two-phase fetch and loadQuick
* perf: optimize data loading across Config, System, Profiles, and Billing tabs
* chore: remove leftover bottom nav references and unused updatePrimaryScreens
* style: fix ktlint violations in Config/Profiles/Billing ViewModels
- property/argument-list wrapping for async(...) calls
- multiline-expression wrapping for async blocks
- remove unused import in ConfigViewModel
- break long error-message string literals to stay under 120 cols
---------
Co-authored-by: M57 <Hy4ri@users.noreply.github.com>
* fix(#548): stop System tab button text truncation + fix two wrong labels
* fix(system): remove inline icons from narrow system action buttons to prevent text cutoff
* fix(system): restore action icons and implement dynamic font scaling for button text
* fix: stack icon above text in system buttons to prevent label clipping
Replace horizontal Icon+Text layout with vertical Column stacking
(icon on top, text centered below) — the same pattern Material 3
NavigationBar items use. Gives text the full button width so labels
like 'Security Audit' and 'Config Migrate' never get cut off.
* fix: add missing ImageVector import for system buttons fix
---------
Co-authored-by: M57 <m57@hermes-control.app>
* 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.
* refactor: migrate Gson serialization to kotlinx.serialization
* fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement
* test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults
* fix(chat): support flexible numeric and string decoding for message timestamps
* fix(chat): sequentialize cache loading to avoid overwriting network state
* feat(#434): add system tab model files
* feat(#434): add system API endpoints
* feat(#434): rewrite SystemViewModel with full parity
* feat(#434): add system screen UI and string resources
* feat(#434): update tests for new SystemStatsResponse & ViewModel
* fix(#434): fix compile errors — nullable Boolean, SmallFilledTonalButton -> FilledTonalButton
* fix(#434): fix old SystemStatsResponse refs in ChatViewModel, wrap sections in item{} blocks
* fix(#434): rename sections to lowercase, fix LazyListScope extensions
* fix(#434): wrap hook dialog in item{}, pass spacing param to actionLogSection
* fix(#434): update HermesApiServiceTest for new typed SystemStatsResponse
* fix(#434): fix E2E test coroutine timing, remove outdated Gson edge-case test
* fix(#434): fix JUnit 4 assertNotNull argument order
* fix(#434): simplify HermesApiServiceTest, restore all mocks in E2e test
* fix(#434): remove Gson-edge-case test, fix E2E test dispatcher wiring
- Removed testGetSystemStats_nonNumericPercent_returnsNull: tests Gson
type-coercion behavior, not app logic — throws JsonSyntaxException
- Fixed E2eIntegrationTest dispatcher: mock Dispatchers.IO with answers {}
that returns the current Dispatchers.Main dynamically, so runTest's
per-test dispatcher override is honored by both viewModelScope.launch
and async(Dispatchers.IO) blocks → advanceUntilIdle() advances all work
- Removed Dispatchers.Main static mock so runTest can override it
* fix(#434): fix E2E test dispatcher for SystemViewModel
- Removed Dispatchers.setMain + testDispatcher field from setUp
- runTest handles Dispatchers.Main automatically — don't override it
- Mock Dispatchers.IO with answers { Dispatchers.Main } so it
dynamically resolves to whatever Main is at call time (runTest's
TestScope dispatcher)
- This ensures viewModelScope.launch and async(Dispatchers.IO) both
run on the same TestScope dispatcher, making advanceUntilIdle()
effective for all work
* fix(#434): use runTest(testDispatcher) for SystemViewModel test
The SystemViewModel.loadAll() uses async(Dispatchers.IO) inside
coroutineScope { }. With the mocked Dispatchers setup, both Main and
IO return testDispatcher, but runTest()'s default TestScope uses a
different dispatcher instance whose advanceUntilIdle() can't advance
testDispatcher's scheduler.
Fix: pass testDispatcher to runTest() so the TestScope's scheduler
matches testDispatcher's scheduler, making advanceUntilIdle()
effective for all async work.
* fix(#434): align test assertion with toast message change
The SystemViewModel.triggerBackup() toast message was changed from
'Backup triggered successfully' to 'Backup triggered', but the
E2eIntegrationTest assertion wasn't updated, causing a
ComparisonFailure on the unit-tests CI job.
* fix(#434): address review issues
- Fix#1: Change triggerBackup() return type from Unit to ActionResponse
so backupArchive is captured, enabling the download button
- Fix#2: Replace deprecated LocalClipboardManager with LocalClipboard
suspend API (removes CI deprecation warning)
- Fix#3: Consolidate redundant 'Hermes version' InfoRow in gateway
section (sessions + PID shown in one row)
- Remove orphaned system_gateway_state_pid string resource
- Fix import ordering for ktlint compliance
* fix(#434): revert LocalClipboard change, use @Suppress('DEPRECATION') instead
LocalClipboard.setText is not available in the Compose version used
by the project. Reverting to LocalClipboardManager with a suppress
annotation to keep the deprecation warning clean without breaking
compilation.
---------
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
Co-authored-by: Hermes Agent <hermes@agent.local>
- Extracted `safeLaunchLoad` inline extension function to `ViewModelExt.kt` to encapsulate API call success/failure handling logic and replaced usages across 18 viewmodels.
- Extracted `ToastHost` interface to standardize `clearToast()` behavior across all ViewModels displaying toasts.
- Extracted `ToastEffect` composable to `SharedComponents.kt` and applied it to 16 screens, reducing repetitive side effect code blocks.
Fixes#286
- HermesScaffold: added isRefreshing param, wraps content in PullToRefreshBox when onRefresh is set
- All 16 data screens: pass isRefreshing = state.isLoading
- Initial loading only shows full-screen LoadingState when data is empty (no spinner over existing content)
- AchievementsScreen already had the correct pattern, just needed isRefreshing param
Co-authored-by: M57 <hy4ri@users.noreply.github.com>
- HermesScaffold: title now @Composable () -> Unit (M3-idiomatic)
- HermesScaffold: added modifier and snackbarHost parameters
- Updated all 18 callers from title="X" to title={Text("X")}
- ChatScreen: converted from raw Scaffold to HermesScaffold
- Removed unused Menu icon import (ktlint fix)
Complete UI/UX rewrite of hermes-mobile with Material You design system.
Phase 1: Color/Theme/Type/Shapes/Spacing/Motion tokens
Phase 2: HermesScaffold, StateViews (skeleton/empty/error), SearchBar, StatusBadge, ToggleRow
Phase 3: Bottom nav + drawer navigation with NavigationController
Phase 4: All 17 screens rewritten with HermesScaffold and Material You patterns
Closes PR #3.