From a61d6180fe997c4d321dc52f6fa47c14b24c6eae Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 20:40:32 +0500 Subject: [PATCH 1/6] search{queries:[ --- feature/search/presentation/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/feature/search/presentation/build.gradle.kts b/feature/search/presentation/build.gradle.kts index 0e79e9ebe..7acc776f4 100644 --- a/feature/search/presentation/build.gradle.kts +++ b/feature/search/presentation/build.gradle.kts @@ -18,6 +18,8 @@ kotlin { implementation(libs.jetbrains.compose.components.resources) implementation(libs.kotlinx.collections.immutable) + + implementation(libs.touchlab.kermit) } } } From 9469214efe9306f668a4ba815c506b8ffdd652d3 Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 20:40:49 +0500 Subject: [PATCH 2/6] search: update exploration logic and UI empty states - Add `passthroughAttempted` to `SearchState` to track if the backend already performed a GitHub search. - Integrate detailed Kermit logging for the "Explore from Github" flow. - Refactor `SearchViewModel` to delegate empty state messaging to the UI and improve pagination logic during exploration. - Update `SearchRoot` to display a centralized "no repositories found" view. - Prevent redundant manual exploration if a passthrough search was already attempted by the backend. --- .../rainxch/search/presentation/SearchRoot.kt | 29 +++++++++ .../search/presentation/SearchState.kt | 1 + .../search/presentation/SearchViewModel.kt | 65 ++++++++++++++----- 3 files changed, 78 insertions(+), 17 deletions(-) diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt index 4dd4be76b..0f3221c76 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchRoot.kt @@ -541,6 +541,35 @@ fun SearchScreen( } } + if (!state.isLoading && + !state.isLoadingMore && + state.errorMessage == null && + state.repositories.isEmpty() && + state.query.isNotBlank() && + !state.hasMorePages + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = stringResource(Res.string.no_repositories_found)) + + // Backend already did its own passthrough and still found + // nothing — don't tease a manual explore that would just + // redo the same work. Any other case (false / null for + // older backends) keeps the CTA. + if (state.passthroughAttempted != true) { + Spacer(Modifier.height(8.dp)) + ExploreFromGithubButton( + status = state.exploreStatus, + onExplore = { onAction(SearchAction.ExploreFromGithub) }, + ) + } + } + } + } + if (state.visibleRepos.isNotEmpty()) { val isScrollbarEnabled = LocalScrollbarEnabled.current ScrollbarContainer( diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt index dc7d46092..306daa79b 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchState.kt @@ -33,6 +33,7 @@ data class SearchState( val autoDetectClipboardEnabled: Boolean = true, val recentSearches: ImmutableList = persistentListOf(), val exploreStatus: ExploreStatus = ExploreStatus.IDLE, + val passthroughAttempted: Boolean? = null, ) { enum class ExploreStatus { IDLE, diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index 7d3d2c14c..c1f80cb9d 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -2,6 +2,7 @@ package zed.rainxch.search.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import co.touchlab.kermit.Logger as KermitLogger import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -39,7 +40,6 @@ import zed.rainxch.githubstore.core.presentation.res.failed_to_share_link import zed.rainxch.githubstore.core.presentation.res.link_copied_to_clipboard import zed.rainxch.githubstore.core.presentation.res.no_github_link_in_clipboard import zed.rainxch.githubstore.core.presentation.res.explore_error -import zed.rainxch.githubstore.core.presentation.res.no_repositories_found import zed.rainxch.githubstore.core.presentation.res.search_failed import zed.rainxch.search.presentation.mappers.toDomain import zed.rainxch.search.presentation.utils.isEntirelyGithubUrls @@ -66,6 +66,8 @@ class SearchViewModel( private var explorePage = 1 private var lastExploreQuery = "" + private val exploreLog = KermitLogger.withTag("SearchExplore") + companion object { private const val MIN_QUERY_LENGTH = 3 } @@ -390,12 +392,8 @@ class SearchViewModel( repositories = allRepos, hasMorePages = paginatedRepos.hasMore, totalCount = allRepos.size, - errorMessage = - if (allRepos.isEmpty() && !paginatedRepos.hasMore) { - getString(Res.string.no_repositories_found) - } else { - null - }, + errorMessage = null, + passthroughAttempted = paginatedRepos.passthroughAttempted, ) } } @@ -682,10 +680,27 @@ class SearchViewModel( private fun performExplore() { val query = _state.value.query.trim() - if (query.isBlank() || _state.value.exploreStatus == SearchState.ExploreStatus.LOADING) return + val platformUi = _state.value.selectedSearchPlatform + val prevStatus = _state.value.exploreStatus + + exploreLog.d { + "click: query='$query' platform=$platformUi " + + "page=$explorePage lastQuery='$lastExploreQuery' status=$prevStatus" + } + + if (query.isBlank()) { + exploreLog.d { "skipped: query is blank" } + return + } + if (prevStatus == SearchState.ExploreStatus.LOADING) { + exploreLog.d { "skipped: already LOADING" } + return + } - // Reset page if query changed if (query != lastExploreQuery) { + exploreLog.d { + "query changed ('$lastExploreQuery' -> '$query'); resetting page to 1" + } explorePage = 1 lastExploreQuery = query } @@ -696,24 +711,40 @@ class SearchViewModel( try { val exploreResult = searchRepository.exploreFromGithub( query = query, - platform = _state.value.selectedSearchPlatform.toDomain(), + platform = platformUi.toDomain(), page = explorePage, ) + val existingCount = _state.value.repositories.size + exploreLog.d { + "response: items=${exploreResult.repos.size} " + + "returnedPage=${exploreResult.page} hasMore=${exploreResult.hasMore} " + + "existingVisible=$existingCount" + } - if (exploreResult.repos.isEmpty() || !exploreResult.hasMore) { - if (exploreResult.repos.isNotEmpty()) { - appendExploreResults(exploreResult.repos) - } - _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.EXHAUSTED) } - } else { + val before = _state.value.repositories.size + if (exploreResult.repos.isNotEmpty()) { appendExploreResults(exploreResult.repos) + } + val added = _state.value.repositories.size - before + val dupes = exploreResult.repos.size - added + + if (exploreResult.hasMore) { explorePage++ + exploreLog.d { + "-> IDLE: appended=$added dupes=$dupes nextPage=$explorePage" + } _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.IDLE) } + } else { + exploreLog.d { + "-> EXHAUSTED: appended=$added dupes=$dupes " + + "rawItems=${exploreResult.repos.size}" + } + _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.EXHAUSTED) } } } catch (e: CancellationException) { throw e } catch (e: Exception) { - logger.error("Explore failed: ${e.message}") + exploreLog.e(e) { "failed: ${e::class.simpleName}: ${e.message}" } _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.IDLE) } _events.send(SearchEvent.OnMessage(getString(Res.string.explore_error))) } From a08120894a71959552af72a262d63bd41132699c Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 20:41:08 +0500 Subject: [PATCH 3/6] BackendApiClient: increase request and socket timeouts for search requests - Update `requestTimeoutMillis` from 20,000 to 30,000ms. - Explicitly set `socketTimeoutMillis` to 30,000ms. --- .../zed/rainxch/core/data/network/BackendApiClient.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt index c5a908a3d..485ef4412 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt @@ -140,7 +140,12 @@ class BackendApiClient( parameter("q", query) if (platform != null) parameter("platform", platform) parameter("page", page) - timeout { requestTimeoutMillis = 20_000 } + + timeout { + requestTimeoutMillis = 30_000 + socketTimeoutMillis = 30_000 + } + if (token != null) header(X_GITHUB_TOKEN_HEADER, token) } if (response.status.isSuccess()) { From 14cda9aa0e12846e24693fee572da7fc43d2b39a Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 20:41:13 +0500 Subject: [PATCH 4/6] add passthroughAttempted field to search response and domain models - Update `BackendSearchResponse` DTO to include an optional `passthroughAttempted` boolean. - Update `PaginatedDiscoveryRepositories` domain model to include `passthroughAttempted`. - Map the new field from the data layer to the domain layer in `SearchRepositoryImpl`. --- .../kotlin/zed/rainxch/core/data/dto/BackendSearchResponse.kt | 1 + .../rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt | 1 + .../zed/rainxch/search/data/repository/SearchRepositoryImpl.kt | 1 + 3 files changed, 3 insertions(+) diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendSearchResponse.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendSearchResponse.kt index 6e6a1c1a5..c65874ee7 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendSearchResponse.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/BackendSearchResponse.kt @@ -8,4 +8,5 @@ data class BackendSearchResponse( val totalHits: Int, val processingTimeMs: Int, val source: String? = null, + val passthroughAttempted: Boolean? = null, ) diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt index 4bc3b2c1d..bcb77defa 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/PaginatedDiscoveryRepositories.kt @@ -8,4 +8,5 @@ data class PaginatedDiscoveryRepositories( val hasMore: Boolean, val nextPageIndex: Int, val totalCount: Int? = null, + val passthroughAttempted: Boolean? = null, ) diff --git a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt index a8e6fa81a..dc1074bcc 100644 --- a/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt +++ b/feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt @@ -147,6 +147,7 @@ class SearchRepositoryImpl( hasMore = hasMore, nextPageIndex = page + 1, totalCount = searchResponse.totalHits, + passthroughAttempted = searchResponse.passthroughAttempted, ) } } From b6829fce63fd283d9152c500310441522a15b12e Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 20:41:17 +0500 Subject: [PATCH 5/6] CLAUDE.md: document desktop logging system and GitHub token forwarding logic - Add documentation for the `CrashReporter` on desktop, including log rotation, crash file generation, and platform-specific log paths. - Detail the security constraints for the `X-GitHub-Token` header, specifying that it is only forwarded to specific search endpoints to utilize user-side rate limits and is never logged. --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 5d91e412b..3410d8d8e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -158,6 +158,8 @@ Custom Gradle plugins in `build-logic/convention/` standardize module setup: - **Shizuku (Android):** Optional silent install via `ShizukuProvider` (registered in AndroidManifest). Requires Shizuku app running with ADB or root. AIDL service passes APK via `ParcelFileDescriptor` to `pm install -S`. Falls back to standard installer on failure. - **Gradle properties:** Config cache enabled, build cache enabled, 4GB Gradle heap, 3GB Kotlin daemon heap - **Code style:** Official Kotlin style (`kotlin.code.style=official`) +- **Desktop logs:** `CrashReporter` (installed as the first line of `DesktopApp.main`) tees `System.out`/`System.err` to a rotating `session.log` and writes `crash-.log` on uncaught exceptions. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Windows), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android uses Logcat — no CrashReporter. +- **`X-GitHub-Token` header:** Forwarded to the backend *only* on `/v1/search` and `/v1/search/explore` (so the backend's live GitHub passthrough runs under the user's own 5000/hr quota). Sourced from `TokenStore.currentToken()` via `BackendApiClient.currentUserGithubToken()` — the helper is `private` so other endpoints can't leak it by accident. Never sent on other endpoints (`/v1/categories`, `/v1/topics`, `/v1/repo`, `/v1/events`), never logged (no Ktor `Logging` plugin installed). ## Coding Conventions From 78c41f6b56da863572c698396eeb590d256a28ce Mon Sep 17 00:00:00 2001 From: rainxchzed Date: Mon, 20 Apr 2026 23:20:32 +0500 Subject: [PATCH 6/6] core: implement tagged logging and refactor search state - Add `withTag` support to `GitHubStoreLogger` and provide a Kermit-based implementation in `TaggedKermitLogger`. - Remove direct Kermit dependency from `feature:search:presentation` in favor of the core logging abstraction. - Update `SearchViewModel` to use the tagged logger for exploration tracing. - Update search state logic to track and reset `passthroughAttempted` status during query changes and initial loads. - Clean up documentation and unused imports. --- CLAUDE.md | 2 +- .../rainxch/core/data/logging/KermitLogger.kt | 25 ++++++++++ .../core/domain/logging/GitHubStoreLogger.kt | 2 + feature/search/presentation/build.gradle.kts | 2 - .../search/presentation/SearchViewModel.kt | 48 +++++++++++-------- 5 files changed, 55 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3410d8d8e..ed4c939f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,7 +159,7 @@ Custom Gradle plugins in `build-logic/convention/` standardize module setup: - **Gradle properties:** Config cache enabled, build cache enabled, 4GB Gradle heap, 3GB Kotlin daemon heap - **Code style:** Official Kotlin style (`kotlin.code.style=official`) - **Desktop logs:** `CrashReporter` (installed as the first line of `DesktopApp.main`) tees `System.out`/`System.err` to a rotating `session.log` and writes `crash-.log` on uncaught exceptions. Paths: `~/Library/Logs/GitHub-Store/` (macOS), `%LOCALAPPDATA%/GitHub-Store/logs/` (Windows), `$XDG_STATE_HOME/GitHub-Store/logs/` (Linux). Android uses Logcat — no CrashReporter. -- **`X-GitHub-Token` header:** Forwarded to the backend *only* on `/v1/search` and `/v1/search/explore` (so the backend's live GitHub passthrough runs under the user's own 5000/hr quota). Sourced from `TokenStore.currentToken()` via `BackendApiClient.currentUserGithubToken()` — the helper is `private` so other endpoints can't leak it by accident. Never sent on other endpoints (`/v1/categories`, `/v1/topics`, `/v1/repo`, `/v1/events`), never logged (no Ktor `Logging` plugin installed). +- **`X-GitHub-Token` header:** Forwarded to the backend *only* on `/v1/search` and `/v1/search/explore` (so the backend's live GitHub passthrough runs under the user's own 5000/hr quota). Sourced from `TokenStore.currentToken()` via `BackendApiClient.currentUserGithubToken()` — the helper is `private` so other endpoints can't leak it accidentally. Never sent on other endpoints (`/v1/categories`, `/v1/topics`, `/v1/repo`, `/v1/events`), never logged (no Ktor `Logging` plugin installed). ## Coding Conventions diff --git a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/logging/KermitLogger.kt b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/logging/KermitLogger.kt index 4a17bfc8b..21148cf47 100644 --- a/core/data/src/commonMain/kotlin/zed/rainxch/core/data/logging/KermitLogger.kt +++ b/core/data/src/commonMain/kotlin/zed/rainxch/core/data/logging/KermitLogger.kt @@ -22,4 +22,29 @@ object KermitLogger : GitHubStoreLogger { ) { Logger.e(message, throwable) } + + override fun withTag(tag: String): GitHubStoreLogger = TaggedKermitLogger(Logger.withTag(tag)) +} + +private class TaggedKermitLogger(private val delegate: Logger) : GitHubStoreLogger { + override fun debug(message: String) { + delegate.d(message) + } + + override fun info(message: String) { + delegate.i(message) + } + + override fun warn(message: String) { + delegate.w(message) + } + + override fun error( + message: String, + throwable: Throwable?, + ) { + delegate.e(message, throwable) + } + + override fun withTag(tag: String): GitHubStoreLogger = TaggedKermitLogger(delegate.withTag(tag)) } diff --git a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/logging/GitHubStoreLogger.kt b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/logging/GitHubStoreLogger.kt index b91808286..cf79055fa 100644 --- a/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/logging/GitHubStoreLogger.kt +++ b/core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/logging/GitHubStoreLogger.kt @@ -11,4 +11,6 @@ interface GitHubStoreLogger { message: String, throwable: Throwable? = null, ) + + fun withTag(tag: String): GitHubStoreLogger = this } diff --git a/feature/search/presentation/build.gradle.kts b/feature/search/presentation/build.gradle.kts index 7acc776f4..0e79e9ebe 100644 --- a/feature/search/presentation/build.gradle.kts +++ b/feature/search/presentation/build.gradle.kts @@ -18,8 +18,6 @@ kotlin { implementation(libs.jetbrains.compose.components.resources) implementation(libs.kotlinx.collections.immutable) - - implementation(libs.touchlab.kermit) } } } diff --git a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt index c1f80cb9d..0fd83e3c7 100644 --- a/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt +++ b/feature/search/presentation/src/commonMain/kotlin/zed/rainxch/search/presentation/SearchViewModel.kt @@ -2,7 +2,6 @@ package zed.rainxch.search.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import co.touchlab.kermit.Logger as KermitLogger import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -66,7 +65,7 @@ class SearchViewModel( private var explorePage = 1 private var lastExploreQuery = "" - private val exploreLog = KermitLogger.withTag("SearchExplore") + private val exploreLog = logger.withTag("SearchExplore") companion object { private const val MIN_QUERY_LENGTH = 3 @@ -297,7 +296,12 @@ class SearchViewModel( currentPage = 1 explorePage = 1 lastExploreQuery = query - _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.IDLE) } + _state.update { + it.copy( + exploreStatus = SearchState.ExploreStatus.IDLE, + passthroughAttempted = null, + ) + } } currentSearchJob = @@ -314,6 +318,8 @@ class SearchViewModel( it.repositories }, totalCount = if (isInitial) null else it.totalCount, + passthroughAttempted = + if (isInitial) null else it.passthroughAttempted, ) } @@ -683,24 +689,24 @@ class SearchViewModel( val platformUi = _state.value.selectedSearchPlatform val prevStatus = _state.value.exploreStatus - exploreLog.d { + exploreLog.debug( "click: query='$query' platform=$platformUi " + - "page=$explorePage lastQuery='$lastExploreQuery' status=$prevStatus" - } + "page=$explorePage lastQuery='$lastExploreQuery' status=$prevStatus", + ) if (query.isBlank()) { - exploreLog.d { "skipped: query is blank" } + exploreLog.debug("skipped: query is blank") return } if (prevStatus == SearchState.ExploreStatus.LOADING) { - exploreLog.d { "skipped: already LOADING" } + exploreLog.debug("skipped: already LOADING") return } if (query != lastExploreQuery) { - exploreLog.d { - "query changed ('$lastExploreQuery' -> '$query'); resetting page to 1" - } + exploreLog.debug( + "query changed ('$lastExploreQuery' -> '$query'); resetting page to 1", + ) explorePage = 1 lastExploreQuery = query } @@ -715,11 +721,11 @@ class SearchViewModel( page = explorePage, ) val existingCount = _state.value.repositories.size - exploreLog.d { + exploreLog.debug( "response: items=${exploreResult.repos.size} " + "returnedPage=${exploreResult.page} hasMore=${exploreResult.hasMore} " + - "existingVisible=$existingCount" - } + "existingVisible=$existingCount", + ) val before = _state.value.repositories.size if (exploreResult.repos.isNotEmpty()) { @@ -730,21 +736,21 @@ class SearchViewModel( if (exploreResult.hasMore) { explorePage++ - exploreLog.d { - "-> IDLE: appended=$added dupes=$dupes nextPage=$explorePage" - } + exploreLog.debug( + "-> IDLE: appended=$added dupes=$dupes nextPage=$explorePage", + ) _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.IDLE) } } else { - exploreLog.d { + exploreLog.debug( "-> EXHAUSTED: appended=$added dupes=$dupes " + - "rawItems=${exploreResult.repos.size}" - } + "rawItems=${exploreResult.repos.size}", + ) _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.EXHAUSTED) } } } catch (e: CancellationException) { throw e } catch (e: Exception) { - exploreLog.e(e) { "failed: ${e::class.simpleName}: ${e.message}" } + exploreLog.error("failed: ${e::class.simpleName}: ${e.message}", e) _state.update { it.copy(exploreStatus = SearchState.ExploreStatus.IDLE) } _events.send(SearchEvent.OnMessage(getString(Res.string.explore_error))) }