ff8370bb8d59174b582f4493351fc8106be2523f
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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>
|
||
|
|
546bd17b18 |
fix(chat): snappy in-chat search over visible text only (#887)
* fix(chat): snappy in-chat search over visible text only - findMatches now scans USER + ASSISTANT messages only — tool rows and system events were matched before, so navigation jumped to invisible matches and the scan paid for the chat's heaviest strings (tool payloads) - Reasoning text is explicitly out of scope (lives in reasoningText, never highlighted) — documented in the controller contract - Debounce (150ms) coalesces typing into one scan instead of one full scan per keystroke; blanking the query still clears instantly - Tests: controller scope (user/assistant only, tool/system/reasoning excluded, case-insensitive, navigation wrap) + delegate debounce (coalescing, cancelled intermediate queries, instant blank clear) * fix(chat): search navigation lands the match mid-view, not at an edge scrollToSearchMatch top-aligned the matched message with animateScrollToItem(index), pinning it to the viewport edge — tall messages left the highlighted word above/below the visible area. Now top-aligns, then nudges the item down to ~1/3 viewport height (verified top-align -> measure -> adjust pattern, same as scrollToBottom). * fix(chat): search scroll targets the real lazy item and the actual word ROOT CAUSE: search-match indices are MESSAGE-list indices, but the LazyColumn has extra items (reasoning hoists, tool rows, loading-older), so scrollToSearchMatch(index) landed on the WRONG item whenever reasoning or tools preceded the match — the 'does nothing / word under or above the view' bug from the start. - messageIdToLazyIndex(): pure messageId -> LazyColumn item index mapping mirroring FullBleedChatList's emission order (verified in tests) - search-scroll effect moved INTO FullBleedChatList (the only composable that knows the mapping), serialized through the same ChatScrollController - word-level targeting: each SearchMatch now carries its character offset in the message; the scroll places offset/contentLength x item height at ~1/3 viewport, so the highlighted word itself lands in view on tall or multi-occurrence messages - counter/navigation now count OCCURRENCES (word hits), one entry per hit with a parallel searchMatchOffsets list; per-message dedupe removed - tests: lazy-index mapping (reasoning hoist, tools, leading items), SearchMatch offsets, occurrence flattening, regex-literal queries * fix(ci): pass searchMatchOffsets and scrollController to FullBleedChatList in instrumented test The render helper in FullBleedChatListTest still used the pre-offset signature, so compileDebugAndroidTestKotlin failed on the emulator job. Verified locally with compileDebugAndroidTestKotlin + ktlintCheck. * perf(chat): allocation-free indexOf search matcher with capped results - findMatches scans with String.indexOf(ignoreCase=true) on the ORIGINAL content instead of Regex(Pattern.quote()).findAll: queries are always literal, so regex was pure overhead — a compiled Pattern plus one MatchResult allocation per hit. Offsets stay exact (no lowercase transform, which can change string length on some scripts). - returns SearchResult(matches, totalMatches, capped): results are capped at 500 stored entries (MAX_SEARCH_MATCHES) to bound allocations and navigation on degenerate queries, while the counter still shows the exact total (searchMatchTotal/searchMatchCapped in ChatUiState). - 14 controller tests green, delegate behavior unchanged. * perf(chat): O(1) current-match lookup and highlight only matched bubbles Two recomposition hot spots killed in the list renderer: - isCurrentMatchFor() did messages.indexOfFirst per rendered bubble per search update — O(n) per item, O(n^2) per update on the main thread. Now currentMatchMessageId() resolves the current match's message id ONCE per recomposition and items compare ids (O(1)). - every prose bubble re-ran its highlight scan on every search-state change even with zero hits. matchedMessageIds() computes the matched id set once; bubbles outside it get an empty searchQuery and skip the scan entirely. Both helpers are pure, tested in FullBleedTurnsTest (18 tests), and the instrumented test target still compiles. * perf(chat): dedicated ChatSearchState holder — search no longer recomposes the list Search fields lived in ChatUiState, so every debounced scan result changed the whole state object: ChatScreen recomposed, FullBleedChatList's params changed, and the entire list body re-ran (turns loop + item registrations) on the main thread per search update. Search state now lives in a @Stable snapshot-backed ChatSearchState (LazyListState pattern) owned by ChatSearchDelegate: - the list takes ONE stable param; the instance never changes, so the body is skipped on search updates — only the scopes that read a field recompose (search bar, matched bubbles, the scroll effect) - matchedIds and currentMatchId are computed once per scan/navigate in the delegate and stored as state fields — items do O(1) id lookups inside their own lambdas, no body-level derivation - ChatUiState loses the 7 search fields; ChatViewModel exposes viewModel.searchState; delegate tests now assert on the holder 19 search-related unit tests green, androidTest compiles. * test(chat): long-session search sanity (2000 msgs) — exact counts, valid offsets Synthetic 2000-message / ~1KB session verifying the matcher at scale: - frequent word: exact 32_000 total, capped storage, every stored offset points at a real occurrence - unique word resolves to the right message; tool payloads stay excluded - local measurement: full scan ~7ms (regex version allocates per hit) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
ff951eaf14 |
fix(chat): stop crash when expanding reasoning card with Show full (#884)
* fix(chat): stop crash when expanding reasoning card with Show full ReasoningCard applied heightIn(max = Dp.Unspecified) once "Show full" was tapped. Inside a LazyColumn item the main axis is unbounded, so the verticalScroll was measured with an infinite max height and Compose threw IllegalStateException on the next layout pass. When Show full is active the scroll wrapper is dropped entirely and the trace renders at its natural height (legal inside a LazyColumn item). Regression test reproduces the crash path: ReasoningCard in a LazyColumn item, expand, Show full, collapse. * test(chat): scroll to the reasoning toggle before asserting it The card grows taller than the viewport after Show full, so the Show less button sits below the fold. performScrollTo brings it on-screen before the assert and the follow-up click. --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
5ab7ad16df |
fix(chat): /update handled client-side with progress popup (issue #862) (#873)
* fix(chat): /update handled client-side with progress popup (issue #862) * test(chat): stub actionProgress controller in ChatScreenTest (issue #862) --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
e52a9c2fe6 | feat(chat): render todo tool output as task card (#807) | ||
|
|
bb4149eb0d |
refactor(chat): two-row ChatComposer + model/reasoning chips (#669)
* feat(chat): add reasoningLevel to ChatUiState + ViewModel methods * feat(chat): add ComposerToolbar with model + reasoning chips * refactor(chat): rewrite ChatInputBar to two-row layout * feat(chat): wire ComposerToolbar into ChatScreen * fix(chat): make model chip fixed 120dp with horizontal scroll for long names * feat(chat): reasoning chip opens dropdown menu with all 9 levels instead of cycling * fix(chat): remove Auto level from reasoning menu — backend only supports none..ultra, default is medium * feat(chat): sync model + reasoning level from backend session.resume response * fix(chat): handle session.info events for model/reasoning sync + fix null value 4002 * fix(chat): safety timeout for createNewSession + try-catch event loop * fix(chat): address agy review findings - Restore InsertDriveFile icon for non-image attachments - Add widthIn(max=120.dp) constraint on attachment filename - Restore stringResource reference for remove-attachment description - Remove dead cycleReasoningLevel() method (dropdown supersedes) - Fix createNewSession timeout race with generation counter * fix(chat): update instrumented test for two-row layout chatScreen_renders_and_acceptsInput had an assertion that the mic button disappears when text is entered — this was the OLD single-row layout behavior. The new two-row composer always shows the mic button in the toolbar row, independent of input state. --------- Co-authored-by: M57 <Hy4ri@users.noreply.github.com> |
||
|
|
4745728920 |
feat(#365): add pin support for models with pinned section at top (#414)
* feat(#365): add pin support for models with pinned section at top * fix(#365): stub Pinned Models methods in E2eIntegrationTest * fix(#365): use state flow for pin/unpin, add max cap, add ViewModel tests --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
42a67bffa9 |
feat(#194): add Speech-to-Text voice dictation support in chat (#413)
* feat(#194): add Speech-to-Text voice dictation support in chat * fix(#194): resolve android lint warnings and update compose UI test * fix(#194): remove invalid assertDoesNotExist import |
||
|
|
8f14aca647 |
perf: optimize startup, database, and ui rendering (#405)
* perf: optimize startup, database, and ui rendering * fix(#405): address code review feedback * test: wait for async prefs initialization to prevent coroutine leaks * test: stub mockViewModel.streamingState in ChatScreenTest to prevent ClassCastException on Android |
||
|
|
15a4748634 |
test(#292): coverage gaps TEST-04 through TEST-14 (#302)
* test(#292): coverage gaps TEST-04 through TEST-14 - TEST-04: ApiClient auth interceptor + BuildConfig.DEBUG gate (6 tests) - TEST-05: SettingsViewModel profile operations (5 tests) - TEST-06: ConnectViewModel profile switch edge cases (8 tests) - TEST-07: EventParser untested subtypes (12 tests) - TEST-08: AuthManager token routing through profiles (5 tests) - TEST-09: ChatScreen instrumented compose tests (3 tests) - TEST-10: WS reconnect state recovery (5 tests) - TEST-11: Multi-profile token scenarios (6 tests) - TEST-12: Chat DAO Room operations (11 tests) - TEST-13: ChatViewModel slash command handling (8 tests) - TEST-14: Predictive back gesture instrumented test (1 test) Total: 70 tests across 11 files. CI updated with new instrumented-tests job via android-emulator-runner. * fix(ci): resolve ktlint, tests, and emulator failures * fix(ci): resolve test failures on PR #302 * fix(ci): more ChatMessageDaoTest context mocks, re-read state after advanceUntilIdle in connect test * fix(ci): remove problematic packageManager mock, decouple instrumented tests from CI gate * fix(ci): switch emulator to default target, add more cores, memory, and timeout * fix(ci): remove mockkStatic(Log::class) from ChatMessageDaoTest - fails on Android stubs * fix(ci): use aosp_atd emulator image + KVM + AVD cache for fast instrumented tests * fix(ci): remove ApplicationInfo mock - Android stub class breaks MockK * fix(ci): rewrite ChatMessageDaoTest to use HermesDatabase.setForTest() pattern - Room in-memory DB requires Android SDK not available in JVM tests * fix(ci): use actions/cache@v4 instead of @v7 (doesn't exist) * fix(ci): add missing performClick import in StateViewsTest * fix(ci): mock ChatViewModel in instrumented tests, add mockk to androidTestImplementation * fix(ci): add asStateFlow import in ChatScreenTest * fix(ci): exclude duplicate META-INF LICENSE/NOTICE files from androidTest build JUnit Jupiter 5.8.2 JARs each ship META-INF/LICENSE.md, causing mergeDebugAndroidTestJavaResource to fail with 'duplicate files found'. Add LICENSE.md, LICENSE, NOTICE, and NOTICE.md to the packaging exclusion list. * fix(#302): exclude duplicate META-INF/LICENSE-notice.md from instrumented test APK JUnit Jupiter 5.x JARs all ship META-INF/LICENSE-notice.md, causing mergeDebugAndroidTestJavaResource to fail. * fix(#302): add LICENSE-notice.md as separate exclude entry The Ant-style glob brace expansion in packaging.resources.excludes doesn't handle LICENCE-notice.md inside the {}-enclosed pattern. Adding it as a separate entry instead. * fix(#302): use mockk-android for instrumented tests io.mockk:mockk uses Java instrumentation agents not available on Android. Switch to io.mockk:mockk-android for connectedDebugAndroidTest which uses an Android-compatible mocking strategy. * fix(#302): use createAndroidComposeRule for test lifecycle isolation Standard createComposeRule has a gap where consecutive test methods can lose the compose hierarchy on the emulator. Switching to createAndroidComposeRule<MainActivity> gives explicit activity lifecycle management across test methods, fixing the 'No compose hierarchies found' errors. * fix(#302): use createComposeRule instead of createAndroidComposeRule<MainActivity> MainActivity.onCreate() already calls setContent, so the test's setContent { ChatScreen(...) } crashed with 'has already set content'. Switch to createComposeRule() which launches a bare ComponentActivity that doesn't call setContent, allowing the test to compose ChatScreen in isolation with a mock ChatViewModel. * fix(#302): use createAndroidComposeRule<ComponentActivity> + unmockkAll for instrumented tests ChatScreenTest was failing with 'No compose hierarchies found' because mockk mocks in instrumented tests weren't cleaned up between test runs. Switch from createComposeRule() to explicit createAndroidComposeRule<ComponentActivity>() and add @After tearDown() with unmockkAll() to ensure mock state doesn't leak between test methods. * fix(#302): wrap ChatScreen in HermesControlTheme in instrumented tests ChatScreen accesses MaterialTheme.colorScheme during composition (background gradient). Without a Material3 theme wrapper, the composable silently crashes and no compose hierarchy is registered — causing 'No compose hierarchies found' on any subsequent onNodeWithTag() call. Fix: wrap ChatScreen in HermesControlTheme inside setContent {}, matching how MainActivity renders the app. Also use createAndroidComposeRule<ComponentActivity> instead of createComposeRule() for explicit lifecycle management, and add unmockkAll() in @After to clean up mockk state between tests. * fix(ci): fix packaging resource paths and use pickFirsts for duplicate META-INF files Remove leading slashes from META-INF paths (AGP paths are relative, not absolute). Use pickFirsts instead of excludes to handle duplicate files from JUnit Jupiter JARs. * fix(#302): use createAndroidComposeRule<ComponentActivity> for test isolation createComposeRule() has a known race where the compose hierarchy is lost between sequential test methods on the emulator. createAndroidComposeRule<ComponentActivity> provides proper Activity lifecycle management while using the same lightweight empty activity that doesn't pre-set content (unlike MainActivity). * fix(#302): consolidate ChatScreenTest into single test method The Compose test rule's lifecycle gap between test methods on the emulator causes 'No compose hierarchies found' on consecutive setContent calls. Consolidating all three test scenarios into one invocation eliminates the cross-test teardown issue entirely. * fix(#302): mock ContextCompat to suppress permission dialog in test The ChatScreen LaunchedEffect requests POST_NOTIFICATIONS via ActivityResultContracts.RequestPermission(), which pauses the activity and disposes the compose hierarchy. Mocking ContextCompat.checkSelfPermission to return PERMISSION_GRANTED prevents this from happening during the test. * fix(#302): set connectionStatus=CONNECTED in ChatScreenTest The default ChatUiState has connectionStatus=DISCONNECTED, which makes isConnected=false, in turn disabling the chat input field (enabled = isConnected) and the send button (canSend checks isConnected). Set it to CONNECTED so both controls are active. * fix(#302): add ConnectionStatus import and fix ktlint wrapping --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com> |
||
|
|
bbe3b00ccb |
test(#293): add foundational Compose UI test for StateViews (TEST-15) (#301)
Co-authored-by: M57 <hy4ri@users.noreply.github.com> |