Skip to content

feat: implement scoped proxy configurations#431

Merged
rainxchzed merged 6 commits intomainfrom
separate-proxy-impl
Apr 19, 2026
Merged

feat: implement scoped proxy configurations#431
rainxchzed merged 6 commits intomainfrom
separate-proxy-impl

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented Apr 18, 2026

  • Introduce ProxyScope to support independent proxy settings for Discovery (GitHub API), Downloads (APK files), and Translation services.
  • Update ProxyRepository and ProxyManager to handle per-scope configurations with legacy migration support.
  • Implement TranslationClientProvider and update BackendApiClient to reactively rebuild HTTP clients when proxy settings change.
  • Refactor repositories to use GitHubClientProvider for dynamic client access.
  • Enhance the UI in the Tweaks section with independent proxy configuration cards for each scope.
  • Update localizations for proxy settings and refine telemetry descriptions across multiple languages.
  • Ensure the telemetry buffer is cleared before resetting the device analytics ID to prevent data leakage.

Summary by CodeRabbit

  • New Features

    • Per-scope proxy settings (Discovery, Downloads, Translation) with startup loading and scoped persistence.
    • Translation provider selection (Google / Youdao) with credential storage and README translation support.
    • Network clients now adapt live when scoped proxy settings change.
  • UI / Tweaks

    • Settings UI refactored to show one proxy card per scope and a new translation provider section with credential form.
  • Localization

    • Added proxy-scope and translation strings and updated telemetry wording across languages.

- Introduce `ProxyScope` to support independent proxy settings for Discovery (GitHub API), Downloads (APK files), and Translation services.
- Update `ProxyRepository` and `ProxyManager` to handle per-scope configurations with legacy migration support.
- Implement `TranslationClientProvider` and update `BackendApiClient` to reactively rebuild HTTP clients when proxy settings change.
- Refactor repositories to use `GitHubClientProvider` for dynamic client access.
- Enhance the UI in the Tweaks section with independent proxy configuration cards for each scope.
- Update localizations for proxy settings and refine telemetry descriptions across multiple languages.
- Ensure the telemetry buffer is cleared before resetting the device analytics ID to prevent data leakage.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 047e8ee3-4128-4ff0-af27-16d7a6c6fa70

📥 Commits

Reviewing files that changed from the base of the PR and between acdb362 and 9967259.

📒 Files selected for processing (1)
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt

Walkthrough

Adds per-feature proxy scopes (DISCOVERY, DOWNLOAD, TRANSLATION), seeds persisted per-scope proxy state at startup, makes HTTP clients reactive to scoped proxy changes via provider types that rebuild clients, updates DI and repositories/downloaders to use client providers, and adds translation-provider selection, translators, UI, and localized strings.

Changes

Cohort / File(s) Summary
Domain model & repo API
core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt, core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt
Add ProxyScope enum; change ProxyRepository API to accept scope for get/set operations.
Proxy manager & seeding
core/data/src/commonMain/kotlin/.../ProxyManager.kt, core/data/src/commonMain/kotlin/.../ProxyManagerSeeding.kt
Replace global flow with per-scope StateFlows and new APIs configFlow(scope), currentConfig(scope), setConfig(scope, ...); add DI seeding marker ProxyManagerSeeding.
Proxy persistence
core/data/src/commonMain/kotlin/.../ProxyRepositoryImpl.kt
Persist/restore scoped proxy keys with legacy fallback; validate/parse stored values; expose getProxyConfig(scope) / setProxyConfig(scope, config) and call ProxyManager.setConfig(scope, ...).
Client providers & backend client
core/data/src/commonMain/kotlin/.../GitHubClientProvider.kt, .../TranslationClientProvider.kt, .../BackendApiClient.kt
Introduce providers that rebuild/replace clients on scoped proxy changes (mutex-protected swap → replace-then-close); BackendApiClient listens to StateFlow<ProxyConfig> and rebuilds its client on updates; added close() methods.
HTTP client / JVM proxy handling
core/data/src/jvmMain/kotlin/.../HttpClientFactory.jvm.kt
Move JVM proxy setup to OkHttp Proxy(...); add conditional HTTP proxyAuthenticator and JVM Authenticator for SOCKS credentials; call Authenticator.setDefault(null) to clear prior global authenticator.
DI & wiring
core/data/src/commonMain/kotlin/.../SharedModule.kt, multiple feature/*/.../di/SharedModule.kt, composeApp/.../initKoin.kt
Seed per-scope proxy configs at startup; switch DI bindings to provide GitHubClientProvider/TranslationClientProvider and update many modules to inject clientProvider = get() instead of httpClient = get().
Repository callsites → provider
many ...RepositoryImpl.kt files (InstalledApps, Starred, Apps, DeveloperProfile, Home, Profile, Search, etc.)
Replace constructor HttpClient with client-provider types and add computed httpClient: HttpClient get() = clientProvider.client so requests use the current rebuilt client.
Downloaders (platform)
core/data/src/androidMain/.../AndroidDownloader.kt, core/data/src/jvmMain/.../DesktopDownloader.kt
Remove proxyManager ctor param; downloaders now query ProxyManager.currentConfig(ProxyScope.DOWNLOAD) when building OkHttp clients.
Tweaks UI / state / actions
feature/tweaks/presentation/.../TweaksAction.kt, TweaksState.kt, TweaksViewModel.kt, model/ProxyScopeFormState.kt, components/sections/Network.kt, components/sections/Translation.kt, components/sections/SettingsSection.kt, TweaksEvent.kt, TweaksRoot.kt
Make proxy actions scope-aware; replace global proxy fields with proxyForms: Map<ProxyScope, ProxyScopeFormState>; ViewModel collects per-scope flows, persists scoped configs, and UI renders one card per scope; add translation provider UI/actions/events and Youdao credential handling.
Translation feature & translators
feature/details/data/.../TranslationRepositoryImpl.kt, .../translation/Translator.kt, GoogleTranslator.kt, YoudaoTranslator.kt
Add Translator abstraction and Google/Youdao implementations (chunking, signing, language mapping); TranslationRepositoryImpl uses TranslationClientProvider, resolves provider per request, and delegates chunked translations.
Tweaks persistence
core/data/src/commonMain/kotlin/.../TweaksRepositoryImpl.kt, core/domain/.../TweaksRepository.kt
Add persistent translation provider and Youdao credentials flows and setters to tweaks repository and interface.
Localization
core/presentation/src/commonMain/composeResources/values*/strings*.xml
Add proxy-scope UI and translation-provider strings across base and many locales; update telemetry_description wording and unescape apostrophes in several locales.

Sequence Diagram(s)

sequenceDiagram
    participant DI as DI
    participant PM as ProxyManager
    participant Provider as ClientProvider
    participant HC as HttpClient
    participant Repo as Repository
    participant Net as Network

    DI->>PM: seed per-scope persisted configs
    PM->>PM: populate configFlow(scope) map

    DI->>Provider: create with configFlow(scope)
    Provider->>HC: build initial HttpClient
    Provider->>Provider: collect configFlow (drop(1), distinctUntilChanged)

    PM->>Provider: emit new ProxyConfig
    Provider->>Provider: mutex lock
    Provider->>HC: create replacement client (with new proxy)
    Provider->>Provider: set currentClient = replacement
    Provider->>HC: close previous client
    Provider->>Provider: mutex unlock

    Repo->>Provider: request client
    Provider-->>Repo: return current HttpClient
    Repo->>Net: execute request (proxy-aware client)
    Net-->>Repo: response
Loading
sequenceDiagram
    participant User as User
    participant UI as Tweaks UI
    participant VM as TweaksViewModel
    participant Repo as ProxyRepository
    participant PM as ProxyManager

    User->>UI: select proxy type for DISCOVERY
    UI->>VM: OnProxyTypeSelected(DISCOVERY,...)
    VM->>Repo: setProxyConfig(DISCOVERY, config)
    Repo->>PM: setConfig(DISCOVERY, config)
    PM->>PM: update configFlow(DISCOVERY)
    VM-->>UI: state updated for DISCOVERY form
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hop through scopes with nimble care,

Discovery, Download, Translation — three little lairs.
Providers rebuild when configs rearrange,
Clients swap gently, the network stays tame.
A rabbit applauds this tidy change!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement scoped proxy configurations' directly summarizes the main change: introducing per-scope proxy configurations for different network operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch separate-proxy-impl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt (1)

16-33: ⚠️ Potential issue | 🟡 Minor

Make formFor safe for partial maps.

getValue can throw if a caller constructs TweaksState(proxyForms = ...) without every ProxyScope; the default only covers constructor calls that omit the argument. A fallback keeps the settings screen from crashing on partial state.

🐛 Proposed fix
-    /** Convenience accessor — guaranteed non-null because the map is
-     *  seeded with entries for every [ProxyScope] at construction time. */
-    fun formFor(scope: ProxyScope): ProxyScopeFormState = proxyForms.getValue(scope)
+    fun formFor(scope: ProxyScope): ProxyScopeFormState =
+        proxyForms[scope] ?: ProxyScopeFormState()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt`
around lines 16 - 33, The formFor accessor currently uses
proxyForms.getValue(scope) which throws if the map is missing an entry; change
formFor to return a safe fallback by looking up proxyForms[scope] and returning
the existing ProxyScopeFormState or a new ProxyScopeFormState() when missing
(use ProxyScope and ProxyScopeFormState references), so callers won’t crash if
TweaksState.proxyForms is partially constructed; optionally consider also
storing the fallback back into the map if you want mutation, but minimally
return the safe default.
🧹 Nitpick comments (7)
feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt (1)

4-4: Unused import.

BackendApiClient is still referenced via backendApiClient = get() through Koin reflection, but the explicit import here only matters if the type is referenced in code. If the BackendApiClient symbol is no longer used directly in this file, remove the import. Otherwise ignore.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt`
at line 4, Remove the unused explicit import of BackendApiClient from
SharedModule.kt if the symbol is not referenced directly; locate the Koin module
where backendApiClient = get() is used and, if BackendApiClient is only injected
via get(), delete the import line for BackendApiClient to eliminate the unused
import. If you still reference the type directly elsewhere in SharedModule.kt,
keep the import; otherwise remove it.
feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt (1)

7-18: Minor: module name vs. feature.

settingsModule is defined in the profile feature; per the retrieved learning, this module is expected to be a profileModule. Not a regression of this PR, but worth aligning when convenient.

As per coding guidelines: "In the feature/profile module, configure Koin dependency injection via data/di/SharedModule.kt with a profileModule".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt`
around lines 7 - 18, The DI module is misnamed: rename the val settingsModule to
profileModule in SharedModule.kt so the feature's Koin module matches the
expected naming convention; update the declaration of settingsModule ->
profileModule and adjust any references/usages where the module is imported or
started (e.g., places that reference settingsModule or loadModule) to use
profileModule, leaving the ProfileRepository binding (ProfileRepositoryImpl)
unchanged.
core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt (1)

49-52: Register an onClose hook for TranslationClientProvider cleanup in DI.

The close() method properly releases the CoroutineScope and HttpClient, but the DI registration at core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt:213–217 creates this singleton without wiring cleanup into Koin's lifecycle. Add an onClose hook to ensure the provider is properly shut down when Koin closes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt`
around lines 49 - 52, The TranslationClientProvider.close() method currently
releases resources but the DI registration in SharedModule doesn't wire that
lifecycle; update the Koin singleton registration that creates
TranslationClientProvider to include an onClose hook that calls
TranslationClientProvider.close() (i.e., when registering the singleton for the
provider in SharedModule, add an onClose {
get<TranslationClientProvider>().close() } or equivalent Koin onClose callback)
so the provider's CoroutineScope and HttpClient are cleaned up when Koin shuts
down.
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt (1)

160-195: Minor: the else branch in the NONE/SYSTEM description is only reachable for NONE.

The outer AnimatedVisibility gates form.type in {NONE, SYSTEM}, so the else in the inner when (form.type) effectively means NONE. This is correct today, but during the exit animation (shrinkVertically() + fadeOut()) the composable recomposes with form.type == HTTP or SOCKS briefly, in which case else -> proxy_none_description shows the "Direct connection, no proxy" label for a frame or two before the card collapses. Consider an explicit ProxyType.NONE arm with a sensible fallback to avoid the flicker during transitions:

                     Text(
                         text =
                             when (form.type) {
                                 ProxyType.SYSTEM -> stringResource(Res.string.proxy_system_description)
-                                else -> stringResource(Res.string.proxy_none_description)
+                                ProxyType.NONE -> stringResource(Res.string.proxy_none_description)
+                                ProxyType.HTTP, ProxyType.SOCKS -> ""
                             },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt`
around lines 160 - 195, The when(form.type) inside the AnimatedVisibility that
shows the description should not rely on an else arm; replace the else with an
explicit ProxyType.NONE branch so the mapping is ProxyType.SYSTEM ->
proxy_system_description and ProxyType.NONE -> proxy_none_description, and keep
a safe fallback (e.g., proxy_system_description or empty string) only outside
that guarded AnimatedVisibility to avoid showing the NONE label during exit
animations; update the when in the AnimatedVisibility block that contains
ProxyTestButton (the one checking form.type == ProxyType.NONE || form.type ==
ProxyType.SYSTEM) to use explicit ProxyType.NONE and ProxyType.SYSTEM arms.
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt (1)

38-44: Minor: use the already-imported Preferences.Key instead of the FQN.

Preferences is imported at the top of the file, so Preferences.Key<String> reads cleaner. Same applies to MutablePreferences in writeOrRemove (line 173).

♻️ Tidy-up
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.MutablePreferences
 import androidx.datastore.preferences.core.intPreferencesKey
 import androidx.datastore.preferences.core.stringPreferencesKey
@@
     private data class ScopeKeys(
-        val type: androidx.datastore.preferences.core.Preferences.Key<String>,
-        val host: androidx.datastore.preferences.core.Preferences.Key<String>,
-        val port: androidx.datastore.preferences.core.Preferences.Key<Int>,
-        val username: androidx.datastore.preferences.core.Preferences.Key<String>,
-        val password: androidx.datastore.preferences.core.Preferences.Key<String>,
+        val type: Preferences.Key<String>,
+        val host: Preferences.Key<String>,
+        val port: Preferences.Key<Int>,
+        val username: Preferences.Key<String>,
+        val password: Preferences.Key<String>,
     )
@@
-    private fun writeOrRemove(
-        prefs: androidx.datastore.preferences.core.MutablePreferences,
-        key: androidx.datastore.preferences.core.Preferences.Key<String>,
-        value: String?,
-    ) {
+    private fun writeOrRemove(
+        prefs: MutablePreferences,
+        key: Preferences.Key<String>,
+        value: String?,
+    ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt`
around lines 38 - 44, Replace the fully-qualified Preferences key types with the
imported shorthand in ScopeKeys and in writeOrRemove: use
Preferences.Key<String> and Preferences.Key<Int> (and MutablePreferences)
instead of androidx.datastore.preferences.core.Preferences.Key<...> to clean up
the types; update the ScopeKeys declaration and the writeOrRemove
parameter/usage to reference MutablePreferences and Preferences.Key where
applicable (symbols to change: ScopeKeys, writeOrRemove, MutablePreferences,
Preferences.Key).
core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt (1)

193-203: Startup blocks up to 4.5 s in the worst case.

Three scopes are seeded sequentially, each with its own withTimeout(1_500L) inside one runBlocking. On a slow/cold DataStore this serializes to ~4.5 s of blocked startup on the Koin initialization thread. Reads across scopes are independent — issuing them concurrently shortens worst-case latency to ~1.5 s.

♻️ Proposed refactor: parallel reads, single overall timeout
-            ProxyScope.entries.forEach { scope ->
-                val saved =
-                    runBlocking {
-                        runCatching {
-                            withTimeout(1_500L) {
-                                repository.getProxyConfig(scope).first()
-                            }
-                        }.getOrDefault(ProxyConfig.System)
-                    }
-                ProxyManager.setConfig(scope, saved)
-            }
+            runBlocking {
+                ProxyScope.entries
+                    .map { scope ->
+                        scope to async {
+                            runCatching {
+                                withTimeout(1_500L) {
+                                    repository.getProxyConfig(scope).first()
+                                }
+                            }.getOrDefault(ProxyConfig.System)
+                        }
+                    }
+                    .forEach { (scope, deferred) ->
+                        ProxyManager.setConfig(scope, deferred.await())
+                    }
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`
around lines 193 - 203, The startup is blocked serially by three per-scope
withTimeout(1_500L) calls inside runBlocking; change to perform the independent
repository.getProxyConfig(scope).first() reads in parallel and apply a single
overall timeout so worst-case latency is ~1_500ms: inside the runBlocking that
currently wraps ProxyScope.entries.forEach, create a coroutine scope and map
each scope to an async { repository.getProxyConfig(scope).first() } task, then
awaitAll the results inside a single withTimeout(1_500L) and call
ProxyManager.setConfig(scope, saved) for each returned result; update references
to runBlocking, withTimeout, repository.getProxyConfig(scope).first(),
ProxyScope.entries and ProxyManager.setConfig to implement this concurrent
pattern.
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt (1)

408-437: Validate port/host only for HTTP/SOCKS in OnProxySave.

OnProxySave runs the port (Line 410-419) and host (Line 420-426) validations unconditionally before the when (form.type) branch. If form.type is NONE/SYSTEM and the form happens to have an empty port/host (e.g., the user never filled them in and the UI still routes a Save action here), the user receives an "invalid port" / "host required" error even though those fields are irrelevant for None/System. In practice NONE/SYSTEM are auto-persisted by OnProxyTypeSelected, so this path is unlikely to fire for them — but a small reorder removes the foot-gun.

♻️ Suggested reordering
             is TweaksAction.OnProxySave -> {
                 val form = _state.value.formFor(action.scope)
+                // NONE/SYSTEM are persisted on type-select and need no
+                // host/port; short-circuit before validating fields.
+                if (form.type == ProxyType.NONE || form.type == ProxyType.SYSTEM) {
+                    val config = if (form.type == ProxyType.NONE) ProxyConfig.None else ProxyConfig.System
+                    viewModelScope.launch {
+                        runCatching { proxyRepository.setProxyConfig(action.scope, config) }
+                            .onSuccess { _events.send(TweaksEvent.OnProxySaved) }
+                            .onFailure { error ->
+                                _events.send(
+                                    TweaksEvent.OnProxySaveError(
+                                        error.message ?: getString(Res.string.failed_to_save_proxy_settings),
+                                    ),
+                                )
+                            }
+                    }
+                    return
+                }
                 val port =
                     form.port
                         .toIntOrNull()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 408 - 437, In TweaksViewModel's OnProxySave handler, move the
form.type check before validating host/port so that NONE and SYSTEM return or
set ProxyConfig directly without triggering port/host validation; for HTTP and
SOCKS only, perform the existing port (toIntOrNull + range) and host
(.trim().isNotBlank()) validations and send TweaksEvent.OnProxySaveError as
before, then build ProxyConfig.Http/ProxyConfig.Socks with username/password —
update the when(form.type) branch and validation order so ProxyType.NONE and
ProxyType.SYSTEM never run the port/host checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`:
- Around line 185-217: The seeding of ProxyManager inside the
GitHubClientProvider factory is order-dependent and can race with
TranslationClientProvider; extract the seeding logic into its own createdAtStart
singleton (e.g., register a sentinel/bean like ProxySeeding) that performs the
runBlocking per-scope reads from ProxyRepository and calls
ProxyManager.setConfig for each ProxyScope, and then make GitHubClientProvider
and TranslationClientProvider depend on that sentinel (inject it into their
factories) so Koin will instantiate the seeding step before either provider
builds their initial HttpClient.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt`:
- Around line 80-83: The BackendApiClient.close() method is never hooked into
Koin shutdown so its httpClient and scope leak; update the DI registration for
single<BackendApiClient> in SharedModule.kt to provide an onClose callback that
calls BackendApiClient.close() (and do the same for GitHubClientProvider and
TranslationClientProvider singletons if they own resources), ensuring the
created instance is closed on Koin stop/teardown.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt`:
- Around line 40-42: The current sequence in mutex.withLock closes currentClient
then assigns a new client, creating a window where readers may observe a closed
client; instead, inside the same mutex block create the replacement first (call
createPlatformHttpClient(config) and store it in a local variable), assign
currentClient to that new instance, then close the old client reference; update
the code paths around mutex, currentClient, createPlatformHttpClient and
currentClient.close() to follow this replace-then-close order.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt`:
- Around line 91-130: The parseConfig function currently downgrades malformed
"http"/"socks" entries to ProxyConfig.None which forces direct traffic; update
parseConfig so that when type == "http" or "socks" and either host is blank or
port invalid it logs a warning via GitHubStoreLogger (include context: type,
host, port) and then returns ProxyConfig.System (not ProxyConfig.None) so OS
proxy rules are honored; locate parseConfig and change the branches that
currently return ProxyConfig.None for Http/Socks to call
GitHubStoreLogger.warn(...) with the details and return ProxyConfig.System
instead.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`:
- Around line 16-65: The SOCKS branch currently installs a process-wide
Authenticator via Authenticator.setDefault(...) whose
getPasswordAuthentication() unconditionally returns credentials, which leaks
proxy creds to any challenge and clobbers other clients; update the anonymous
Authenticator in the ProxyConfig.Socks handling (the object : Authenticator()
with override fun getPasswordAuthentication()) to validate that requestingHost
== proxyConfig.host and requestingPort == proxyConfig.port before returning
PasswordAuthentication(username, password.toCharArray()), and return null
otherwise (so credentials are only supplied to the configured proxy).

In `@core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml`:
- Line 632: Update the source English string for telemetry_description to
replace "anonymized usage data" with a more accurate term like "pseudonymous
usage data" or "de-identified usage data", then update this localized Arabic
string (string name telemetry_description) to use the corresponding Arabic
phrasing (e.g., replace "مجهولة الهوية" with an appropriate translation such as
"بيانات معرفة بشكل مستعار" or "بيانات منزوعة الهوية") so the translation matches
the revised English source and accurately reflects that data is tied to a
resettable analytics ID.

In `@core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml`:
- Line 631: Update the translation for the string resource
telemetry_description: replace the term "বেনামী" with a Bengali term that
reflects "pseudonymous/de-identified" (e.g., "উপনামভিত্তিক" or "অপচিহ্নিত"), so
the sentence accurately reads that usage data tied to a resettable analytics ID
is pseudonymized/de-identified rather than anonymous; edit the value of the
<string name="telemetry_description"> accordingly while preserving the rest of
the sentence about sharing usage (search, install, interaction) and not sharing
account details.

In `@core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml`:
- Line 593: Update the French telemetry string (string name
telemetry_description) to use the correct privacy term: replace "anonymisées"
with "pseudonymisées" so the description reads that usage data is pseudonymized
(tied to a resettable analytics ID); keep existing punctuation and escaped
apostrophes as in the current value.

In `@core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml`:
- Line 630: The string resource telemetry_description currently uses "अनाम"
which overstates privacy; update the value of the string named
telemetry_description to describe the data as usage/interaction data associated
with a resettable analytics ID (e.g., "रीसेट करने योग्य एनालिटिक्स आईडी से जुड़ा
उपयोग डेटा (खोज, इंस्टॉल, इंटरैक्शन) साझा करें") and keep the existing
account-details disclaimer intact so it no longer calls the ID-linked telemetry
"anonymous."

In `@core/presentation/src/commonMain/composeResources/values-it/strings-it.xml`:
- Line 631: Update the Italian telemetry string to avoid implying full
anonymity: edit the string resource named telemetry_description and replace
“anonimizzati” with wording that states events are associated with a resettable
analytics ID (e.g., “associati a un ID analitico ripristinabile” or similar),
keeping the rest of the sentence about searches, installs and interactions and
“Nessun dettaglio dell'account viene condiviso.” to preserve meaning.

In `@core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml`:
- Line 594: Update the telemetry description string named
"telemetry_description" to use "pseudonymous" terminology: replace the Japanese
phrase "匿名化された" with the correct pseudonymous term (e.g., "仮名化された" or "準匿名化された")
in the strings-ja.xml entry and update the English source string (the
telemetry_description in values/strings.xml) from "anonymized" to "pseudonymous"
so all locales consistently reflect that the data is tied to a resettable
analytics ID.

In `@core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml`:
- Line 595: Update the translation for the string resource
"telemetry_description" to avoid implying full anonymization; replace
"zanonimizowane" with the Polish equivalent of "pseudonymized" or
"de-identified" (e.g., "pseudonimizowane" or "zdeidentyfikowane") so the string
correctly reads that usage data is pseudonymized/de-identified and linked to a
resettable analytics identifier; ensure the updated text preserves the rest of
the original wording and tone.

In `@core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml`:
- Line 595: Update the localized telemetry string with language that reflects
pseudonymization rather than complete anonymization: edit the string with name
"telemetry_description" and replace "обезличенные данные" (or similar phrasing)
with a phrase like "псевдонимизированные данные" and adjust surrounding wording
to state that data are linked to a сбрасываемый идентификатор аналитики
(resettable analytics ID) and that account-level data are not transmitted;
ensure the new Russian sentence remains grammatically correct and preserves the
original meaning about searches, installs, and interactions.

In `@core/presentation/src/commonMain/composeResources/values/strings.xml`:
- Line 651: The string resource telemetry_description currently uses the term
“anonymized” which overstates privacy because events are tied to an analytics
ID; update the resource value for string name "telemetry_description" to remove
“anonymized” and instead state that usage data are "associated with" or "linked
to a resettable analytics ID" while keeping the "No account details are shared"
clause; locate the string with name telemetry_description and replace its text
accordingly (e.g., "Share usage data (searches, installs, interactions)
associated with a resettable analytics ID. No account details are shared.").

---

Outside diff comments:
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt`:
- Around line 16-33: The formFor accessor currently uses
proxyForms.getValue(scope) which throws if the map is missing an entry; change
formFor to return a safe fallback by looking up proxyForms[scope] and returning
the existing ProxyScopeFormState or a new ProxyScopeFormState() when missing
(use ProxyScope and ProxyScopeFormState references), so callers won’t crash if
TweaksState.proxyForms is partially constructed; optionally consider also
storing the fallback back into the map if you want mutation, but minimally
return the safe default.

---

Nitpick comments:
In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`:
- Around line 193-203: The startup is blocked serially by three per-scope
withTimeout(1_500L) calls inside runBlocking; change to perform the independent
repository.getProxyConfig(scope).first() reads in parallel and apply a single
overall timeout so worst-case latency is ~1_500ms: inside the runBlocking that
currently wraps ProxyScope.entries.forEach, create a coroutine scope and map
each scope to an async { repository.getProxyConfig(scope).first() } task, then
awaitAll the results inside a single withTimeout(1_500L) and call
ProxyManager.setConfig(scope, saved) for each returned result; update references
to runBlocking, withTimeout, repository.getProxyConfig(scope).first(),
ProxyScope.entries and ProxyManager.setConfig to implement this concurrent
pattern.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt`:
- Around line 49-52: The TranslationClientProvider.close() method currently
releases resources but the DI registration in SharedModule doesn't wire that
lifecycle; update the Koin singleton registration that creates
TranslationClientProvider to include an onClose hook that calls
TranslationClientProvider.close() (i.e., when registering the singleton for the
provider in SharedModule, add an onClose {
get<TranslationClientProvider>().close() } or equivalent Koin onClose callback)
so the provider's CoroutineScope and HttpClient are cleaned up when Koin shuts
down.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt`:
- Around line 38-44: Replace the fully-qualified Preferences key types with the
imported shorthand in ScopeKeys and in writeOrRemove: use
Preferences.Key<String> and Preferences.Key<Int> (and MutablePreferences)
instead of androidx.datastore.preferences.core.Preferences.Key<...> to clean up
the types; update the ScopeKeys declaration and the writeOrRemove
parameter/usage to reference MutablePreferences and Preferences.Key where
applicable (symbols to change: ScopeKeys, writeOrRemove, MutablePreferences,
Preferences.Key).

In
`@feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt`:
- Around line 7-18: The DI module is misnamed: rename the val settingsModule to
profileModule in SharedModule.kt so the feature's Koin module matches the
expected naming convention; update the declaration of settingsModule ->
profileModule and adjust any references/usages where the module is imported or
started (e.g., places that reference settingsModule or loadModule) to use
profileModule, leaving the ProfileRepository binding (ProfileRepositoryImpl)
unchanged.

In
`@feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt`:
- Line 4: Remove the unused explicit import of BackendApiClient from
SharedModule.kt if the symbol is not referenced directly; locate the Koin module
where backendApiClient = get() is used and, if BackendApiClient is only injected
via get(), delete the import line for BackendApiClient to eliminate the unused
import. If you still reference the type directly elsewhere in SharedModule.kt,
keep the import; otherwise remove it.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt`:
- Around line 160-195: The when(form.type) inside the AnimatedVisibility that
shows the description should not rely on an else arm; replace the else with an
explicit ProxyType.NONE branch so the mapping is ProxyType.SYSTEM ->
proxy_system_description and ProxyType.NONE -> proxy_none_description, and keep
a safe fallback (e.g., proxy_system_description or empty string) only outside
that guarded AnimatedVisibility to avoid showing the NONE label during exit
animations; update the when in the AnimatedVisibility block that contains
ProxyTestButton (the one checking form.type == ProxyType.NONE || form.type ==
ProxyType.SYSTEM) to use explicit ProxyType.NONE and ProxyType.SYSTEM arms.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 408-437: In TweaksViewModel's OnProxySave handler, move the
form.type check before validating host/port so that NONE and SYSTEM return or
set ProxyConfig directly without triggering port/host validation; for HTTP and
SOCKS only, perform the existing port (toIntOrNull + range) and host
(.trim().isNotBlank()) validations and send TweaksEvent.OnProxySaveError as
before, then build ProxyConfig.Http/ProxyConfig.Socks with username/password —
update the when(form.type) branch and validation order so ProxyType.NONE and
ProxyType.SYSTEM never run the port/host checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d354661c-1008-469a-ad23-6057cf551d23

📥 Commits

Reviewing files that changed from the base of the PR and between 39ccd4f and 94d9fe5.

📒 Files selected for processing (42)
  • core/data/src/androidMain/kotlin/zed/rainxch/core/data/services/AndroidDownloader.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManager.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/InstalledAppsRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/StarredRepositoryImpl.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopDownloader.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/ProxyScope.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/ProxyRepository.kt
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/di/SharedModule.kt
  • feature/apps/data/src/commonMain/kotlin/zed/rainxch/apps/data/repository/AppsRepositoryImpl.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/di/SharedModule.kt
  • feature/dev-profile/data/src/commonMain/kotlin/zed/rainxch/devprofile/data/repository/DeveloperProfileRepositoryImpl.kt
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/di/SharedModule.kt
  • feature/home/data/src/commonMain/kotlin/zed/rainxch/home/data/repository/HomeRepositoryImpl.kt
  • feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt
  • feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/repository/ProfileRepositoryImpl.kt
  • feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt
  • feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/repository/SearchRepositoryImpl.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt

Comment thread core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt Outdated
Comment on lines +80 to 83
fun close() {
httpClient.close()
scope.cancel()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

close() is defined but never invoked via Koin.

BackendApiClient owns a CoroutineScope and an HttpClient but its DI registration (single<BackendApiClient> { … } in SharedModule.kt lines 146-150) has no onClose callback, so on Koin shutdown/test teardown the coroutine subscriber and underlying engine leak. Consider wiring a teardown hook.

🔒 Proposed fix in SharedModule.kt
-        single<BackendApiClient> {
-            BackendApiClient(
-                proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY),
-            )
-        }
+        single<BackendApiClient> {
+            BackendApiClient(
+                proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY),
+            )
+        } onClose { it?.close() }

Same treatment would be appropriate for the GitHubClientProvider and TranslationClientProvider singletons if not already covered elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/BackendApiClient.kt`
around lines 80 - 83, The BackendApiClient.close() method is never hooked into
Koin shutdown so its httpClient and scope leak; update the DI registration for
single<BackendApiClient> in SharedModule.kt to provide an onClose callback that
calls BackendApiClient.close() (and do the same for GitHubClientProvider and
TranslationClientProvider singletons if they own resources), ensuring the
created instance is closed on Koin stop/teardown.

Comment thread core/presentation/src/commonMain/composeResources/values-it/strings-it.xml Outdated
<string name="privacy_section">プライバシー</string>
<string name="telemetry_title">検索の改善に協力</string>
<string name="telemetry_description">推奨を改善するため、匿名の使用データ(検索、インストール)を共有します。個人情報は収集されません。</string>
<string name="telemetry_description">リセット可能な分析 ID に関連付けられた匿名化された使用データ(検索、インストール、操作)を共有します。アカウント情報は共有されません。</string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find the file in question
find . -name "strings-ja.xml" -type f

Repository: OpenHub-Store/GitHub-Store

Length of output: 146


🏁 Script executed:

# Check if the file exists and verify content at line 594
if [ -f "core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml" ]; then
  echo "=== Content around line 594 ===" 
  sed -n '590,600p' "core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml"
fi

Repository: OpenHub-Store/GitHub-Store

Length of output: 773


🏁 Script executed:

# Find the English version to compare terminology
find . -name "strings.xml" -type f | head -5

Repository: OpenHub-Store/GitHub-Store

Length of output: 192


🏁 Script executed:

# Find the English telemetry description
grep -n "telemetry_description" "core/presentation/src/commonMain/composeResources/values/strings.xml"

Repository: OpenHub-Store/GitHub-Store

Length of output: 252


🏁 Script executed:

# Get context around the telemetry strings in English version
sed -n '580,610p' "core/presentation/src/commonMain/composeResources/values/strings.xml"

Repository: OpenHub-Store/GitHub-Store

Length of output: 2271


🏁 Script executed:

# Check for other analytics/telemetry related strings and how they're translated
grep -n "analytics_id\|telemetry\|anonymous" "core/presentation/src/commonMain/composeResources/values/strings.xml" | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 632


🏁 Script executed:

# Check the same in Japanese version
grep -n "analytics_id\|telemetry\|anonymous\|匿名" "core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml"

Repository: OpenHub-Store/GitHub-Store

Length of output: 478


🏁 Script executed:

# Search for any privacy policy or documentation that might clarify data handling
find . -type f \( -name "*.md" -o -name "*privacy*" -o -name "*policy*" \) | head -10

Repository: OpenHub-Store/GitHub-Store

Length of output: 304


🏁 Script executed:

# Check if there are other locales that might have different terminology
find core/presentation/src/commonMain/composeResources -name "strings*.xml" -type f | sort

Repository: OpenHub-Store/GitHub-Store

Length of output: 1046


🏁 Script executed:

# Check a couple other locales to see how they handle telemetry_description
grep -A1 "telemetry_description" core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
grep -A1 "telemetry_description" core/presentation/src/commonMain/composeResources/values-es/strings-es.xml

Repository: OpenHub-Store/GitHub-Store

Length of output: 679


Update telemetry description to use "pseudonymous" instead of "anonymous".

The data is tied to a resettable analytics ID, which means events remain linkable until the ID is reset. This makes the data pseudonymous, not anonymous. Using "anonymous" or "anonymized" is misleading from a privacy and compliance perspective.

This issue also affects the English source (line 651 of core/presentation/src/commonMain/composeResources/values/strings.xml): "Share anonymized usage data (searches, installs, interactions) tied to a resettable analytics ID." The Japanese translation accurately reflects this terminology, but the terminology itself needs correction across all locales.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml`
at line 594, Update the telemetry description string named
"telemetry_description" to use "pseudonymous" terminology: replace the Japanese
phrase "匿名化された" with the correct pseudonymous term (e.g., "仮名化された" or "準匿名化された")
in the strings-ja.xml entry and update the English source string (the
telemetry_description in values/strings.xml) from "anonymized" to "pseudonymous"
so all locales consistently reflect that the data is tied to a resettable
analytics ID.

Comment thread core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml Outdated
<string name="privacy_section">Конфиденциальность</string>
<string name="telemetry_title">Помочь улучшить поиск</string>
<string name="telemetry_description">Отправлять анонимные данные об использовании (поиски, установки) для улучшения рекомендаций. Личная информация не собирается.</string>
<string name="telemetry_description">Отправлять обезличенные данные об использовании (поиски, установки, взаимодействия), привязанные к сбрасываемому идентификатору аналитики. Данные учётной записи не передаются.</string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the strings-ru.xml file around the mentioned line
cat -n core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml | sed -n '590,600p'

Repository: OpenHub-Store/GitHub-Store

Length of output: 1103


🏁 Script executed:

# Let's also check if there are other language versions to compare translations
fd -e xml -path '*composeResources/values-*/strings*' | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 238


🏁 Script executed:

# Search for telemetry-related strings across language files
rg "telemetry_description" core/presentation/src/commonMain/composeResources/

Repository: OpenHub-Store/GitHub-Store

Length of output: 3462


🏁 Script executed:

# Let's check if there are any discussions or issues about this telemetry terminology in the codebase
rg -i "pseudonymous|pseudonym" core/presentation/

Repository: OpenHub-Store/GitHub-Store

Length of output: 52


🏁 Script executed:

# Also check for any privacy-related documentation or comments
rg -i "privacy|telemetry" core/presentation/src/commonMain/composeResources/ | grep -i "comment\|description" | head -10

Repository: OpenHub-Store/GitHub-Store

Length of output: 2503


Use pseudonymous or de-identified terminology instead of anonymized.

Data linked to a resettable analytics ID cannot be characterized as fully anonymized, as it remains traceable and correlatable through that ID. The term "обезличенные" partially mitigates this, but the wording still risks misleading users about the correlation capabilities. Use terminology that accurately reflects the pseudonymous nature of the data collection (e.g., "псевдонимизированные данные" or similar).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml`
at line 595, Update the localized telemetry string with language that reflects
pseudonymization rather than complete anonymization: edit the string with name
"telemetry_description" and replace "обезличенные данные" (or similar phrasing)
with a phrase like "псевдонимизированные данные" and adjust surrounding wording
to state that data are linked to a сбрасываемый идентификатор аналитики
(resettable analytics ID) and that account-level data are not transmitted;
ensure the new Russian sentence remains grammatically correct and preserves the
original meaning about searches, installs, and interactions.

Comment thread core/presentation/src/commonMain/composeResources/values/strings.xml Outdated
…for Youdao as an alternative to Google Translate.

- Implement a `Translator` interface and refactor translation logic into specialized `GoogleTranslator` and `YoudaoTranslator` classes.
- Add `YoudaoTranslator` to support users in mainland China, requiring a user-provided `appKey` and `appSecret`.
- Update `TranslationRepositoryImpl` to dynamically resolve the selected translator and handle provider-specific chunk size limits.
- Expand the Tweaks/Settings UI with a new Translation section to allow provider selection and credential management for Youdao.
- Update `TweaksRepository` and `TweaksViewModel` to persist and manage translation settings.
- Add localized strings for the new translation settings in English, Turkish, Arabic, Chinese (Simplified), Bengali, Japanese, Korean, Polish, Italian, French, Russian, Hindi, and Spanish.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml (1)

594-594: ⚠️ Potential issue | 🟠 Major

Use pseudonymous terminology instead of “anonymous/anonymized”.

This data is tied to a resettable analytics ID, so events remain linkable until reset. 匿名化された overstates the privacy guarantee; use wording like 仮名化された or 準匿名化された consistently with the source string.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml`
at line 594, Update the Japanese telemetry string identified by
name="telemetry_description" to replace the term "匿名化された" with a pseudonymous
term such as "仮名化された" or "準匿名化された" so the wording reflects that data is tied to
a resettable analytics ID; ensure the new phrasing remains consistent with the
source string and other localized telemetry strings.
🧹 Nitpick comments (2)
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt (1)

17-38: formFor will throw on any externally-constructed map missing a scope.

proxyForms.getValue(scope) throws NoSuchElementException if a caller ever .copy(proxyForms = …) with an incomplete map (e.g. after adding a new ProxyScope enum entry without updating constructors in tests/migrations). Since the default seeds all entries this is fine today, but a safer accessor would defensively fall back:

Optional hardening
-    fun formFor(scope: ProxyScope): ProxyScopeFormState = proxyForms.getValue(scope)
+    fun formFor(scope: ProxyScope): ProxyScopeFormState =
+        proxyForms[scope] ?: ProxyScopeFormState()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt`
around lines 17 - 38, The current formFor(scope) uses proxyForms.getValue(scope)
which will throw if someone copies an incomplete map; change it to defensively
return a fallback ProxyScopeFormState when the key is missing (e.g. use
proxyForms[scope] or proxyForms.getOrElse(scope) to return a new
ProxyScopeFormState), keeping the rest of the data class (proxyForms seeded via
ProxyScope.entries.associateWith) intact so callers never get a
NoSuchElementException.
core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt (1)

183-212: Implementation looks good.

DataStore-backed persistence with trim-on-write and removal-on-empty for Youdao credentials is consistent with the rest of the repository and handles the common "cleared credentials" case correctly.

One minor consideration: Youdao App Key/Secret are sensitive credentials stored as plain string preferences. DataStore preferences aren't encrypted at rest — if threat modeling for this feature includes device compromise or backup exfiltration, consider wrapping these in an encrypted store (e.g., AndroidX Security EncryptedFile / platform Keystore-backed storage). Not blocking for this PR.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt`
around lines 183 - 212, You are currently storing Youdao credentials in plain
DataStore preferences via getYoudaoAppKey/setYoudaoAppKey and
getYoudaoAppSecret/setYoudaoAppSecret; replace that with an encrypted storage
layer: implement an EncryptionProvider or use platform-specific secure storage
(e.g., AndroidX Security EncryptedSharedPreferences or Keystore-backed
encryption) and update these four methods to read/write through that encryption
wrapper instead of writing raw strings to preferences, preserving trim-on-write
and remove-on-empty behavior and leaving getTranslationProvider/etc. unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt`:
- Around line 21-38: GoogleTranslator currently exposes maxChunkSize = 4500 but
uses GET with the text as a query param in translate(...), which breaks because
chunkText (in TranslationRepositoryImpl) only measures String.length and
URL-encoding expands non-ASCII dramatically; fix by reducing maxChunkSize for
this provider to a safe encoded-aware value (e.g., ~1000) and/or change
GoogleTranslator.translate to use a POST request (sending text in the body) so
URL length isn’t an issue—update the GoogleTranslator.maxChunkSize and/or
replace the GET call in GoogleTranslator.translate(...) with a POST-based
request (or implement an encoded-length-aware chunking helper that computes
URLEncoder.encode(text).length before chunking) so chunkText behavior aligns
with actual encoded length.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt`:
- Line 7: YoudaoTranslator currently uses java.security.MessageDigest which is
JVM-only; replace the direct use by adding an internal expect function (e.g.
internal expect fun sha256HexCompat(input: String): String) in commonMain and
call that from YoudaoTranslator instead of MessageDigest, then provide platform
actual implementations (on JVM implement using MessageDigest, on JS/Native use a
multiplatform crypto lib or platform APIs). Ensure the symbol sha256HexCompat is
referenced in YoudaoTranslator where hashing is required and remove the
java.security.MessageDigest import from the common file.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 600-607: The OnTranslationProviderSelected handler currently
persists any provider immediately (via tweaksRepository.setTranslationProvider)
which allows selecting YOUDAO with no credentials; change the handler so that if
action.provider == YOUDAO you first check for saved credentials (e.g., via an
existing tweaksRepository.hasYoudaoCredentials / getYoudaoCredentials method)
and only call tweaksRepository.setTranslationProvider and send
TweaksEvent.OnTranslationProviderSaved when credentials exist; if credentials
are missing, store the selection as a draft field on TweaksViewModel (e.g.,
pendingYoudaoProvider) and send a different event (or prompt) asking the user to
save credentials, then persist the provider only from the
OnYoudaoCredentialsSave flow once credentials are saved; keep immediate
persistence for Google and other providers unchanged.
- Around line 170-225: loadProxyConfig currently overwrites form state on every
preferences emission, which clobbers in-progress edits; modify loadProxyConfig
and the form state handling to skip hydrating a scope when that scope has an
active draft (e.g., add a per-scope "isDraftDirty" flag in ProxyScopeFormState
or a separate Set<ProxyScope> tracking edited scopes and check it in
loadProxyConfig before applying persisted config), only applying persisted
ProxyConfig into state.proxyForms for a scope when isDraftDirty is false (or on
explicit OnProxySave/OnProxyCancel actions), and update mutateForm to mark the
scope as dirty when the user edits so hydration is deferred until commit/cancel.

---

Duplicate comments:
In `@core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml`:
- Line 594: Update the Japanese telemetry string identified by
name="telemetry_description" to replace the term "匿名化された" with a pseudonymous
term such as "仮名化された" or "準匿名化された" so the wording reflects that data is tied to
a resettable analytics ID; ensure the new phrasing remains consistent with the
source string and other localized telemetry strings.

---

Nitpick comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt`:
- Around line 183-212: You are currently storing Youdao credentials in plain
DataStore preferences via getYoudaoAppKey/setYoudaoAppKey and
getYoudaoAppSecret/setYoudaoAppSecret; replace that with an encrypted storage
layer: implement an EncryptionProvider or use platform-specific secure storage
(e.g., AndroidX Security EncryptedSharedPreferences or Keystore-backed
encryption) and update these four methods to read/write through that encryption
wrapper instead of writing raw strings to preferences, preserving trim-on-write
and remove-on-empty behavior and leaving getTranslationProvider/etc. unchanged.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt`:
- Around line 17-38: The current formFor(scope) uses proxyForms.getValue(scope)
which will throw if someone copies an incomplete map; change it to defensively
return a fallback ProxyScopeFormState when the key is missing (e.g. use
proxyForms[scope] or proxyForms.getOrElse(scope) to return a new
ProxyScopeFormState), keeping the rest of the data class (proxyForms seeded via
ProxyScope.entries.associateWith) intact so callers never get a
NoSuchElementException.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4052dca1-9756-4095-9cc3-4b7d2691bab8

📥 Commits

Reviewing files that changed from the base of the PR and between 94d9fe5 and 53f096b.

📒 Files selected for processing (28)
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/TweaksRepositoryImpl.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/TweaksRepository.kt
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/Translator.kt
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksAction.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksRoot.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/SettingsSection.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt
✅ Files skipped from review due to trivial changes (3)
  • core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/TranslationProvider.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksEvent.kt
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
🚧 Files skipped from review as they are similar to previous changes (9)
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/di/SharedModule.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/repository/TranslationRepositoryImpl.kt
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml

import io.ktor.client.request.forms.submitForm
import io.ktor.client.statement.bodyAsText
import io.ktor.http.Parameters
import java.security.MessageDigest
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Verify JVM-only java.* imports under commonMain.
# Expected after the fix: no matches for java.security.MessageDigest in commonMain Kotlin sources.

fd -e kt . | rg '/commonMain/' | xargs rg -n '^\s*import\s+java\.|MessageDigest\.getInstance'

Repository: OpenHub-Store/GitHub-Store

Length of output: 2380


Move SHA-256 behind expect/actual or a multiplatform crypto API.

This file is in commonMain, but java.security.MessageDigest is JVM-only and will break non-JVM/common metadata compilation. Keep YoudaoTranslator common and delegate hashing to an expect function with platform actual implementations, or use an existing multiplatform crypto library.

🔧 Sketch of the common-side change
-import java.security.MessageDigest
@@
-    private fun sha256Hex(input: String): String {
-        val digest = MessageDigest.getInstance("SHA-256").digest(input.encodeToByteArray())
-        return buildString(digest.size * 2) {
-            for (byte in digest) {
-                val v = byte.toInt() and 0xff
-                if (v < 0x10) append('0')
-                append(v.toString(16))
-            }
-        }
-    }
+    private fun sha256Hex(input: String): String = sha256HexCompat(input)

Then define internal expect fun sha256HexCompat(input: String): String in commonMain and provide platform actual implementations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/YoudaoTranslator.kt`
at line 7, YoudaoTranslator currently uses java.security.MessageDigest which is
JVM-only; replace the direct use by adding an internal expect function (e.g.
internal expect fun sha256HexCompat(input: String): String) in commonMain and
call that from YoudaoTranslator instead of MessageDigest, then provide platform
actual implementations (on JVM implement using MessageDigest, on JS/Native use a
multiplatform crypto lib or platform APIs). Ensure the symbol sha256HexCompat is
referenced in YoudaoTranslator where hashing is required and remove the
java.security.MessageDigest import from the common file.

…etry strings

- Introduce `ProxyManagerSeeding` as a marker type to ensure explicit DI dependency ordering, preventing races between HTTP client creation and proxy configuration loading.
- Optimize proxy seeding to run DataStore reads in parallel with a unified 1.5s timeout.
- Implement "replace-then-close" logic in `GitHubClientProvider` and `TranslationClientProvider` to ensure callers always observe a live HTTP client.
- Update `ProxyRepositoryImpl` to fallback to `ProxyConfig.System` and log a warning when encountering malformed configurations.
- Refine proxy validation in `TweaksViewModel` to skip host/port requirements for "None" and "System" types.
- Rename `settingsModule` to `profileModule` for better domain consistency.
- Update `telemetry_description` across multiple locales (EN, TR, AR, ZH, BN, JA, KO, PL, IT, FR, RU, HI, ES) to remove explicit "anonymized" wording in favor of describing the association with a resettable ID.
- Enhance SOCKS5 authentication on JVM to scope credentials to the specific proxy host and port, preventing credential leakage to other requests.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt (2)

170-225: ⚠️ Potential issue | 🟠 Major

Keep persisted proxy hydration from overwriting in-progress edits.

loadProxyConfig() still writes repository emissions into the same proxyForms entries that OnProxy*Changed mutates. If a scope emits while the user has unsaved HTTP/SOCKS edits, the draft host/port/credentials can be reverted. Track per-scope dirty/draft state and skip hydration until save/cancel, or separate persisted config from editable form state.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 170 - 225, loadProxyConfig currently overwrites user edits because
repository emissions update the same proxyForms entries that
mutateForm/OnProxy*Changed update; change the model to separate persisted config
from editable draft per scope (e.g., add fields on ProxyScopeFormState like
isDirty or draft vs persistedConfig) and update loadProxyConfig to early-return
or skip updating proxyForms when the corresponding form state is dirty, while
mutateForm/OnProxy*Changed should set isDirty=true and save/cancel handlers
should clear dirty and merge persistedConfig into the editable form; update
references in loadProxyConfig, mutateForm, ProxyScopeFormState, proxyForms and
the OnProxy*Changed handlers accordingly.

617-655: ⚠️ Potential issue | 🟠 Major

Preserve the invariant that YOUDAO is only active with saved credentials.

Line 622 persists any provider immediately, so selecting YOUDAO can activate it with blank credentials. Lines 644-655 can also save blank credentials while leaving TranslationProvider.YOUDAO active. Gate Youdao activation on non-blank saved credentials, or keep Youdao as a draft selection until credentials are saved; if credentials are cleared while active, switch to a safe provider or reject the save.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 617 - 655, Selecting or saving the YOUDAO provider can leave it
active without non-blank saved credentials; update the handlers to prevent that:
in the TweaksAction.OnTranslationProviderSelected branch, if action.provider ==
TranslationProvider.YOUDAO, only call
tweaksRepository.setTranslationProvider(...) when the current saved credentials
(from _state.value.youdaoAppKey and youdaoAppSecret) are both non-blank
(otherwise keep selection as a draft or no-op); in
TweaksAction.OnYoudaoCredentialsSave (where setYoudaoAppKey and
setYoudaoAppSecret are called), after persisting credentials only switch to
TranslationProvider.YOUDAO if the saved values are non-blank (and if credentials
were cleared while TranslationProvider.YOUDAO is active, call
tweaksRepository.setTranslationProvider(...) with a safe fallback like
TranslationProvider.AUTO_DETECT or the previous provider). Ensure checks
reference _state.value.youdaoAppKey, _state.value.youdaoAppSecret,
tweaksRepository.setTranslationProvider, and the
OnYoudaoCredentialsSave/OnTranslationProviderSelected branches.
core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt (1)

17-21: ⚠️ Potential issue | 🟠 Major

Avoid clearing/replacing the JVM-wide SOCKS authenticator per client.

Authenticator.setDefault(...) is process-wide and used at request time. With scoped proxy configs, a later None/System/Http client build clears credentials for an already-live SOCKS client, and a second SOCKS client replaces the first one. The host/port guard helps leakage, but not multi-client coexistence. Also include requestorType == RequestorType.PROXY before returning proxy credentials.

Prefer one process-wide SOCKS authenticator registry, updated by the scope-aware provider/manager, rather than clearing it during each HttpClient build.

#!/bin/bash
# Description: Verify all JVM-wide Authenticator mutations and client rebuild paths.
# Expectation: Authenticator.setDefault should be centralized, not called per scoped client construction.

rg -n -C4 'Authenticator\.setDefault|class .*Authenticator|object .*Authenticator|getPasswordAuthentication|createPlatformHttpClient|configFlow\(' \
  --type=kt
Java Authenticator.setDefault process-wide default authenticator requestorType PROXY getPasswordAuthentication documentation

Also applies to: 63-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`
around lines 17 - 21, The code must not call Authenticator.setDefault(null)
per-client in HttpClientFactory.jvm.kt; instead implement a process-wide SOCKS
authenticator registry/manager that is set once and updated by scoped
providers—remove the per-build Authenticator.setDefault call in
createPlatformHttpClient/HttpClientFactory and wire the new
SocksAuthenticatorRegistry (or SocksAuthenticatorProvider) to
register/unregister credentials for each ProxyConfig.Socks scope; also ensure
the registry's getPasswordAuthentication implementation checks requestorType ==
RequestorType.PROXY before returning credentials (and uses host/port guards) so
multiple SOCKS clients can coexist without clearing global state.
🧹 Nitpick comments (1)
core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt (1)

160-169: Move the PR-review rationale out of production source.

The note explains a review conversation and a version-specific Koin limitation; it will age better in the PR, an issue, or an ADR. Keep source comments focused on the current design constraint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`
around lines 160 - 169, Remove the PR-review rationale from the SharedModule.kt
comment and replace it with a concise production-focused note about the current
design: state that Koin 4.x's module DSL lacks an onClose hook so we
intentionally do not register shutdown hooks for BackendApiClient.close(),
GitHubClientProvider.close(), and TranslationClientProvider.close(), and mention
to revisit this decision if we upgrade Koin or adopt scope-based DI; move the
longer review rationale into the PR description, an issue, or an ADR instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`:
- Around line 214-233: The seeding currently wraps the whole awaitAll in
runCatching so one failing getProxyConfig(scope).first() cancels all tasks;
change the async block inside the coroutineScope (within ProxyManagerSeeding) to
catch exceptions per scope: in the map { async { ... } } return a safe wrapper
like Result or Pair<ProxyScope, ProxyConfig?> (e.g., try { scope to
repository.getProxyConfig(scope).first() } catch(e: Throwable) { scope to null }
), then awaitAll(), filter out null configs, and call
ProxyManager.setConfig(scope, config) only for successful pairs so one bad scope
won't prevent others from being applied. Ensure you still respect the existing
withTimeout and propagate/log individual errors as needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt`:
- Around line 45-58: The client-swap race occurs because `previous.close()` is
invoked immediately after `currentClient` is replaced (seen in
`GitHubClientProvider` around `currentClient = replacement` and
`previous.close()`), allowing a thread that read the old `currentClient` to
start a request just before closure; fix by deferring disposal: after assigning
`currentClient = replacement`, schedule closing `previous` on the provider's
`scope` (or a short suspend delay/drain period, e.g., launch a coroutine that
delays ~500–1000ms or until an in-flight-counter drops to zero) and then call
`previous.close()` inside that coroutine so existing requests can complete;
apply the identical change in `TranslationClientProvider` (the same
`currentClient`/`previous.close()` swap) and ensure the Mutex protecting the
swap remains unchanged.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`:
- Around line 36-44: The proxyAuthenticator currently always builds a new
request with Proxy-Authorization using Credentials.basic(username, password),
which causes OkHttp to repeatedly retry the same credentials after a failed
challenge; update the lambda in proxyAuthenticator to first check
response.request.headers("Proxy-Authorization") (or
response.request.header("Proxy-Authorization")) and if that header is already
present return null to stop further retries, otherwise create the new request
with .newBuilder().header("Proxy-Authorization", Credentials.basic(username,
password)).build().

In
`@core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml`:
- Around line 595-597: The current string reset_analytics_id_description still
uses "匿名 ID" which conflicts with the updated telemetry wording; update the
value of the string resource reset_analytics_id_description (and optionally
review reset_analytics_id_title) to remove the "匿名 ID" anonymity claim and
instead use neutral wording like "生成新的 ID,切断与过去遥测数据的联系" so it aligns with
telemetry_description's updated phrasing.

---

Duplicate comments:
In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`:
- Around line 17-21: The code must not call Authenticator.setDefault(null)
per-client in HttpClientFactory.jvm.kt; instead implement a process-wide SOCKS
authenticator registry/manager that is set once and updated by scoped
providers—remove the per-build Authenticator.setDefault call in
createPlatformHttpClient/HttpClientFactory and wire the new
SocksAuthenticatorRegistry (or SocksAuthenticatorProvider) to
register/unregister credentials for each ProxyConfig.Socks scope; also ensure
the registry's getPasswordAuthentication implementation checks requestorType ==
RequestorType.PROXY before returning credentials (and uses host/port guards) so
multiple SOCKS clients can coexist without clearing global state.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 170-225: loadProxyConfig currently overwrites user edits because
repository emissions update the same proxyForms entries that
mutateForm/OnProxy*Changed update; change the model to separate persisted config
from editable draft per scope (e.g., add fields on ProxyScopeFormState like
isDirty or draft vs persistedConfig) and update loadProxyConfig to early-return
or skip updating proxyForms when the corresponding form state is dirty, while
mutateForm/OnProxy*Changed should set isDirty=true and save/cancel handlers
should clear dirty and merge persistedConfig into the editable form; update
references in loadProxyConfig, mutateForm, ProxyScopeFormState, proxyForms and
the OnProxy*Changed handlers accordingly.
- Around line 617-655: Selecting or saving the YOUDAO provider can leave it
active without non-blank saved credentials; update the handlers to prevent that:
in the TweaksAction.OnTranslationProviderSelected branch, if action.provider ==
TranslationProvider.YOUDAO, only call
tweaksRepository.setTranslationProvider(...) when the current saved credentials
(from _state.value.youdaoAppKey and youdaoAppSecret) are both non-blank
(otherwise keep selection as a draft or no-op); in
TweaksAction.OnYoudaoCredentialsSave (where setYoudaoAppKey and
setYoudaoAppSecret are called), after persisting credentials only switch to
TranslationProvider.YOUDAO if the saved values are non-blank (and if credentials
were cleared while TranslationProvider.YOUDAO is active, call
tweaksRepository.setTranslationProvider(...) with a safe fallback like
TranslationProvider.AUTO_DETECT or the previous provider). Ensure checks
reference _state.value.youdaoAppKey, _state.value.youdaoAppSecret,
tweaksRepository.setTranslationProvider, and the
OnYoudaoCredentialsSave/OnTranslationProviderSelected branches.

---

Nitpick comments:
In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`:
- Around line 160-169: Remove the PR-review rationale from the SharedModule.kt
comment and replace it with a concise production-focused note about the current
design: state that Koin 4.x's module DSL lacks an onClose hook so we
intentionally do not register shutdown hooks for BackendApiClient.close(),
GitHubClientProvider.close(), and TranslationClientProvider.close(), and mention
to revisit this decision if we upgrade Koin or adopt scope-based DI; move the
longer review rationale into the PR description, an issue, or an ADR instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: da323f9b-32c8-4577-8474-499c77234047

📥 Commits

Reviewing files that changed from the base of the PR and between 53f096b and 5512f2d.

📒 Files selected for processing (24)
  • composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt
  • core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • core/presentation/src/commonMain/composeResources/values-bn/strings-bn.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
  • core/presentation/src/commonMain/composeResources/values-hi/strings-hi.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-ko/strings-ko.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-tr/strings-tr.xml
  • core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt
  • feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
✅ Files skipped from review due to trivial changes (3)
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/ProxyManagerSeeding.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt
  • core/presentation/src/commonMain/composeResources/values-fr/strings-fr.xml
🚧 Files skipped from review as they are similar to previous changes (11)
  • feature/profile/data/src/commonMain/kotlin/zed/rainxch/profile/data/di/SharedModule.kt
  • core/presentation/src/commonMain/composeResources/values-ru/strings-ru.xml
  • core/presentation/src/commonMain/composeResources/values-pl/strings-pl.xml
  • core/presentation/src/commonMain/composeResources/values-ja/strings-ja.xml
  • core/presentation/src/commonMain/composeResources/values-es/strings-es.xml
  • core/presentation/src/commonMain/composeResources/values-it/strings-it.xml
  • core/presentation/src/commonMain/composeResources/values-ar/strings-ar.xml
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • core/presentation/src/commonMain/composeResources/values/strings.xml
  • feature/search/data/src/commonMain/kotlin/zed/rainxch/search/data/di/SharedModule.kt
  • core/data/src/commonMain/kotlin/zed/rainxch/core/data/repository/ProxyRepositoryImpl.kt

Comment on lines +214 to 233
single<ProxyManagerSeeding>(createdAtStart = true) {
val repository = get<ProxyRepository>()
runBlocking {
runCatching {
withTimeout(1_500L) {
coroutineScope {
ProxyScope.entries
.map { scope ->
async {
scope to repository.getProxyConfig(scope).first()
}
}.awaitAll()
}
}.getOrDefault(ProxyConfig.System)
}

when (config) {
is ProxyConfig.None -> {
ProxyManager.setNoProxy()
}

is ProxyConfig.System -> {
ProxyManager.setSystemProxy()
}

is ProxyConfig.Http -> {
ProxyManager.setHttpProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password,
)
}

is ProxyConfig.Socks -> {
ProxyManager.setSocksProxy(
host = config.host,
port = config.port,
username = config.username,
password = config.password,
)
}
}.onSuccess { results ->
results.forEach { (scope, config) ->
ProxyManager.setConfig(scope, config)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt | sed -n '200,240p'

Repository: OpenHub-Store/GitHub-Store

Length of output: 2077


🏁 Script executed:

# Check the package declaration and overall file structure
head -20 core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 917


🏁 Script executed:

# Look for ProxyConfig class to verify the default value
rg "class ProxyConfig|object ProxyConfig" -A 5

Repository: OpenHub-Store/GitHub-Store

Length of output: 669


🏁 Script executed:

# Check if this coroutineScope has SupervisorJob context
sed -n '214,235p' core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 939


🏁 Script executed:

# Verify that onSuccess is indeed the only place ProxyManager.setConfig is called in this context
rg "ProxyManager\.setConfig" -B 5 -A 2

Repository: OpenHub-Store/GitHub-Store

Length of output: 1894


🏁 Script executed:

# Check if there's any top-level supervision in the Koin module
sed -n '202,250p' core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 2207


Isolate seeding failures per proxy scope.

One failed getProxyConfig(scope).first() causes awaitAll() to throw, the runCatching catches it, and onSuccess never executes—leaving all scopes unconfigured (not even the in-memory defaults are applied). This contradicts the comment's intent to fall back to System proxy on failure. Failures should be caught per async task before awaiting so successful scopes are still applied.

🛡️ Proposed fix to preserve successful scope reads
             runBlocking {
                 runCatching {
                     withTimeout(1_500L) {
                         coroutineScope {
                             ProxyScope.entries
                                 .map { scope ->
                                     async {
-                                        scope to repository.getProxyConfig(scope).first()
+                                        val config =
+                                            runCatching {
+                                                repository.getProxyConfig(scope).first()
+                                            }.getOrDefault(ProxyConfig.System)
+
+                                        scope to config
                                     }
                                 }.awaitAll()
                         }
                     }
                 }.onSuccess { results ->
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.kt`
around lines 214 - 233, The seeding currently wraps the whole awaitAll in
runCatching so one failing getProxyConfig(scope).first() cancels all tasks;
change the async block inside the coroutineScope (within ProxyManagerSeeding) to
catch exceptions per scope: in the map { async { ... } } return a safe wrapper
like Result or Pair<ProxyScope, ProxyConfig?> (e.g., try { scope to
repository.getProxyConfig(scope).first() } catch(e: Throwable) { scope to null }
), then awaitAll(), filter out null configs, and call
ProxyManager.setConfig(scope, config) only for successful pairs so one bad scope
won't prevent others from being applied. Ensure you still respect the existing
withTimeout and propagate/log individual errors as needed.

Comment on lines +45 to +58
// Replace-then-close: readers of [client] always see
// a live client. Closing first opens a window where
// an in-flight call could touch a closed engine.
val replacement =
createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
authenticationState = authenticationState,
scope = scope,
proxyConfig = proxyConfig,
)
val previous = currentClient
currentClient = replacement
previous.close()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find and read the GitHubClientProvider.kt file
find . -name "GitHubClientProvider.kt" -type f

Repository: OpenHub-Store/GitHub-Store

Length of output: 157


🏁 Script executed:

# Also search for TranslationClientProvider.kt
find . -name "TranslationClientProvider.kt" -type f

Repository: OpenHub-Store/GitHub-Store

Length of output: 162


🏁 Script executed:

# Check the structure of the core/data directory
fd --type f --extension kt "core/data" | head -20

Repository: OpenHub-Store/GitHub-Store

Length of output: 411


🏁 Script executed:

# Read the GitHubClientProvider.kt file to verify the code snippet
cat -n ./core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 3056


🏁 Script executed:

# Read the TranslationClientProvider.kt file to check for similar pattern
cat -n ./core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 2553


Delay disposal of previous to close the remaining client-swap race.

The Mutex protects the client swap itself, but a thread can read currentClient before the assignment and start a request on it immediately before previous.close() is called. Since Ktor's close() starts asynchronous shutdown, requests initiated just before closure can fail. Implement a drain period before closing to let inflight requests complete.

This same race exists in TranslationClientProvider.kt (lines 45–47) and requires the identical fix.

🛡️ Possible drain-period fix
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.sync.Mutex
 import kotlinx.coroutines.sync.withLock
@@
 class GitHubClientProvider(
@@
 ) {
+    private companion object {
+        const val CLIENT_REPLACEMENT_DRAIN_MS = 30_000L
+    }
+
@@
                     val previous = currentClient
                     currentClient = replacement
-                    previous.close()
+                    scope.launch {
+                        try {
+                            delay(CLIENT_REPLACEMENT_DRAIN_MS)
+                        } finally {
+                            previous.close()
+                        }
+                    }
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Replace-then-close: readers of [client] always see
// a live client. Closing first opens a window where
// an in-flight call could touch a closed engine.
val replacement =
createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
authenticationState = authenticationState,
scope = scope,
proxyConfig = proxyConfig,
)
val previous = currentClient
currentClient = replacement
previous.close()
// Replace-then-close: readers of [client] always see
// a live client. Closing first opens a window where
// an in-flight call could touch a closed engine.
val replacement =
createGitHubHttpClient(
tokenStore = tokenStore,
rateLimitRepository = rateLimitRepository,
authenticationState = authenticationState,
scope = scope,
proxyConfig = proxyConfig,
)
val previous = currentClient
currentClient = replacement
scope.launch {
try {
delay(CLIENT_REPLACEMENT_DRAIN_MS)
} finally {
previous.close()
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.kt`
around lines 45 - 58, The client-swap race occurs because `previous.close()` is
invoked immediately after `currentClient` is replaced (seen in
`GitHubClientProvider` around `currentClient = replacement` and
`previous.close()`), allowing a thread that read the old `currentClient` to
start a request just before closure; fix by deferring disposal: after assigning
`currentClient = replacement`, schedule closing `previous` on the provider's
`scope` (or a short suspend delay/drain period, e.g., launch a coroutine that
delays ~500–1000ms or until an in-flight-counter drops to zero) and then call
`previous.close()` inside that coroutine so existing requests can complete;
apply the identical change in `TranslationClientProvider` (the same
`currentClient`/`previous.close()` swap) and ensure the Mutex protecting the
swap remains unchanged.

Comment on lines +36 to +44
if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) {
proxyAuthenticator { _, response ->
response.request
.newBuilder()
.header(
"Proxy-Authorization",
Credentials.basic(username, password),
).build()
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

OkHttp proxyAuthenticator return null when credentials already attempted Proxy-Authorization documentation

💡 Result:

In OkHttp, the ProxyAuthenticator (an implementation of Authenticator) should return null from the authenticate(Route?, Response) method when credentials have already been attempted, to prevent infinite retry loops and give up on authentication. This is the standard pattern documented in the official OkHttp Authenticator documentation. For proxy authentication (HTTP 407 responses), check if the request already has a "Proxy-Authorization" header: if (response.request.header("Proxy-Authorization") != null) { return null; // Give up, we've already failed to authenticate. } String credential = Credentials.basic(username, password); return response.request.newBuilder .header("Proxy-Authorization", credential) .build; The Proxy-Authorization header carries proxy credentials in the format specified by RFC 7235 (similar to Authorization but for proxies). OkHttp adds this header to the CONNECT request for HTTPS tunneling or regular requests through HTTP proxies. Returning null declines the authentication challenge, causing OkHttp to propagate the 407 response to the application rather than retrying. This behavior applies to both reactive authentication (after 407) and preemptive authentication (fake 407 with "OkHttp-Preemptive" challenge before CONNECT), where returning null skips preemptive auth.

Citations:


🏁 Script executed:

cat -n core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt

Repository: OpenHub-Store/GitHub-Store

Length of output: 4597


Add a guard to prevent retrying identical proxy credentials after a failed challenge.

When the proxy rejects credentials, the proxyAuthenticator keeps retrying with the same request until OkHttp hits its follow-up limit. Return null when Proxy-Authorization was already attempted to stop the retry cycle.

Proposed fix
                         val password = proxyConfig.password
                         if (!username.isNullOrEmpty() && !password.isNullOrEmpty()) {
                             proxyAuthenticator { _, response ->
+                                if (response.request.header("Proxy-Authorization") != null) {
+                                    return@proxyAuthenticator null
+                                }
                                 response.request
                                     .newBuilder()
                                     .header(
                                         "Proxy-Authorization",
                                         Credentials.basic(username, password),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/data/src/jvmMain/kotlin/zed/rainxch/core/data/network/HttpClientFactory.jvm.kt`
around lines 36 - 44, The proxyAuthenticator currently always builds a new
request with Proxy-Authorization using Credentials.basic(username, password),
which causes OkHttp to repeatedly retry the same credentials after a failed
challenge; update the lambda in proxyAuthenticator to first check
response.request.headers("Proxy-Authorization") (or
response.request.header("Proxy-Authorization")) and if that header is already
present return null to stop further retries, otherwise create the new request
with .newBuilder().header("Proxy-Authorization", Credentials.basic(username,
password)).build().

Comment on lines +595 to 597
<string name="telemetry_description">分享与可重置分析 ID 关联的使用数据(搜索、安装、交互)。不分享账户详情。</string>
<string name="reset_analytics_id_title">重置分析 ID</string>
<string name="reset_analytics_id_description">生成新的匿名 ID,切断与过去遥测数据的联系。</string>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align the reset-ID copy with the new privacy wording.

Line 597 still describes the identifier as 匿名 ID, so users can still see an anonymity claim next to the updated telemetry disclosure. Consider removing that wording here too.

📝 Proposed wording update
-    <string name="reset_analytics_id_description">生成新的匿名 ID,切断与过去遥测数据的联系。</string>
+    <string name="reset_analytics_id_description">生成新的分析 ID,切断与过去遥测数据的联系。</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<string name="telemetry_description">分享与可重置分析 ID 关联的使用数据(搜索、安装、交互)。不分享账户详情。</string>
<string name="reset_analytics_id_title">重置分析 ID</string>
<string name="reset_analytics_id_description">生成新的匿名 ID,切断与过去遥测数据的联系。</string>
<string name="telemetry_description">分享与可重置分析 ID 关联的使用数据(搜索、安装、交互)。不分享账户详情。</string>
<string name="reset_analytics_id_title">重置分析 ID</string>
<string name="reset_analytics_id_description">生成新的分析 ID,切断与过去遥测数据的联系。</string>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@core/presentation/src/commonMain/composeResources/values-zh-rCN/strings-zh-rCN.xml`
around lines 595 - 597, The current string reset_analytics_id_description still
uses "匿名 ID" which conflicts with the updated telemetry wording; update the
value of the string resource reset_analytics_id_description (and optionally
review reset_analytics_id_title) to remove the "匿名 ID" anonymity claim and
instead use neutral wording like "生成新的 ID,切断与过去遥测数据的联系" so it aligns with
telemetry_description's updated phrasing.

- **Proxy Settings**: Introduce `isDraftDirty` flag to `ProxyScopeFormState` to prevent background DataStore emissions from overwriting in-progress form edits.
- **Translation Provider**:
    - Implement `draftTranslationProvider` in `TweaksState` to handle providers requiring configuration (e.g., Youdao) before persistence.
    - Update UI to reflect the draft selection while credentials are being entered.
    - Automatically commit the translation provider when valid credentials are saved.
- **Google Translator**:
    - Switch from `GET` to `POST` (form-encoded) for translation requests to handle large payloads (CJK characters, etc.) that exceed URL length limits.
    - Update documentation regarding chunk size and encoding expansion.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt (1)

332-347: ⚠️ Potential issue | 🟠 Major

Separate Youdao draft credentials from persisted credentials before activating the provider.

hasCreds checks editable state fields, so after choosing Youdao and typing unsaved credentials, tapping the chip again can persist YOUDAO while the repository still has blank/old credentials. The same shared fields are also hydrated by repository collectors, so unrelated preference emissions can overwrite in-progress credential edits. Track credential draft dirtiness or separate persisted vs draft values, and only activate Youdao from known-saved credentials or immediately after a successful credentials save.

Suggested direction
         viewModelScope.launch {
             tweaksRepository.getYoudaoAppKey().collect { appKey ->
-                _state.update { it.copy(youdaoAppKey = appKey) }
+                _state.update {
+                    if (it.isYoudaoCredentialsDirty) {
+                        it
+                    } else {
+                        it.copy(youdaoAppKey = appKey)
+                    }
+                }
             }
         }
         viewModelScope.launch {
             tweaksRepository.getYoudaoAppSecret().collect { appSecret ->
-                _state.update { it.copy(youdaoAppSecret = appSecret) }
+                _state.update {
+                    if (it.isYoudaoCredentialsDirty) {
+                        it
+                    } else {
+                        it.copy(youdaoAppSecret = appSecret)
+                    }
+                }
             }
         }
             is TweaksAction.OnTranslationProviderSelected -> {
                 when (action.provider) {
@@
                     TranslationProvider.YOUDAO -> {
                         val current = _state.value
                         val hasCreds =
+                            !current.isYoudaoCredentialsDirty &&
                             current.youdaoAppKey.isNotBlank() &&
                                 current.youdaoAppSecret.isNotBlank()
             is TweaksAction.OnYoudaoAppKeyChanged -> {
-                _state.update { it.copy(youdaoAppKey = action.appKey) }
+                _state.update {
+                    it.copy(
+                        youdaoAppKey = action.appKey,
+                        isYoudaoCredentialsDirty = true,
+                    )
+                }
             }
 
             is TweaksAction.OnYoudaoAppSecretChanged -> {
-                _state.update { it.copy(youdaoAppSecret = action.appSecret) }
+                _state.update {
+                    it.copy(
+                        youdaoAppSecret = action.appSecret,
+                        isYoudaoCredentialsDirty = true,
+                    )
+                }
             }

Also clear isYoudaoCredentialsDirty only after both credential writes succeed.

Also applies to: 646-724

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 332 - 347, The collectors in loadTranslationSettings are conflating
persisted and in-progress Youdao credentials so UI edits can transiently
activate YOUDAO or get overwritten by repository emissions; change the state
model to separate persisted vs draft fields (e.g.,
youdaoAppKeyPersisted/youdaoAppSecretPersisted vs
youdaoAppKeyDraft/youdaoAppSecretDraft) and track an isYoudaoCredentialsDirty
flag in TweaksViewModel; update the collectors from tweaksRepository
(getYoudaoAppKey/getYoudaoAppSecret) to populate the persisted fields only,
update UI edits to write to the draft fields and set isYoudaoCredentialsDirty,
and only switch translationProvider to YOUDAO (or persist changes to repository)
when credentials are saved successfully, clearing isYoudaoCredentialsDirty only
after both credential writes succeed so activation uses known-saved credentials
and repository emissions won’t overwrite in-progress edits.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 225-237: mutateForm currently always sets
ProxyScopeFormState.isDraftDirty = true (via
block(state.formFor(scope)).copy(isDraftDirty = true)), which incorrectly marks
UI-only changes (password visibility toggle, proxy-test progress) as dirty; add
a new helper (e.g., mutateFormUi or mutateFormTransient) that updates proxyForms
using state.formFor(scope) without setting isDraftDirty and replace calls that
only change transient flags (password visibility, testInProgress/testResult) to
use that helper, leaving mutateForm for real config edits; update uses of
state.formFor, ProxyScopeFormState, and any callers that toggle the eye icon or
run proxy tests to prevent blocking repository hydration.

---

Duplicate comments:
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 332-347: The collectors in loadTranslationSettings are conflating
persisted and in-progress Youdao credentials so UI edits can transiently
activate YOUDAO or get overwritten by repository emissions; change the state
model to separate persisted vs draft fields (e.g.,
youdaoAppKeyPersisted/youdaoAppSecretPersisted vs
youdaoAppKeyDraft/youdaoAppSecretDraft) and track an isYoudaoCredentialsDirty
flag in TweaksViewModel; update the collectors from tweaksRepository
(getYoudaoAppKey/getYoudaoAppSecret) to populate the persisted fields only,
update UI edits to write to the draft fields and set isYoudaoCredentialsDirty,
and only switch translationProvider to YOUDAO (or persist changes to repository)
when credentials are saved successfully, clearing isYoudaoCredentialsDirty only
after both credential writes succeed so activation uses known-saved credentials
and repository emissions won’t overwrite in-progress edits.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9768cbed-ff0f-464a-8be1-882d38041077

📥 Commits

Reviewing files that changed from the base of the PR and between 5512f2d and acdb362.

📒 Files selected for processing (5)
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Translation.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • feature/details/data/src/commonMain/kotlin/zed/rainxch/details/data/translation/GoogleTranslator.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/model/ProxyScopeFormState.kt
  • feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksState.kt

…on for transient state

- Add `mutateFormUi` to handle transient UI state changes (password visibility, test-in-progress) without marking the scope as dirty.
- Update `OnProxyPasswordVisibilityToggle` and `OnProxyTestRequest` to use `mutateFormUi`.
- Ensure that toggling UI elements or running tests does not prevent the form from re-hydrating during preference emissions.
@rainxchzed rainxchzed merged commit f9f8768 into main Apr 19, 2026
1 check was pending
@rainxchzed rainxchzed deleted the separate-proxy-impl branch April 19, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant