Skip to content

fix: single-tap save proxy + suspend getString + preserve cancellation#578

Merged
rainxchzed merged 2 commits into
mainfrom
fix/proxy-save-doubletap
May 12, 2026
Merged

fix: single-tap save proxy + suspend getString + preserve cancellation#578
rainxchzed merged 2 commits into
mainfrom
fix/proxy-save-doubletap

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

@rainxchzed rainxchzed commented May 11, 2026

Two fixes (one a follow-up to merged #576, one a supersede of open #577):

1. Save proxy required two taps. Root cause: the enabled = isFormValid && !form.isTestInProgress gate raced with the IME-composition-commit step on Android keyboards (Gboard / SwiftKey hold last-typed chars in a composition buffer until focus actually transfers). First tap landed on a momentarily-disabled button. Removed the validity gate — VM still validates and surfaces a clear error event. Also added LocalSoftwareKeyboardController.hide() alongside focusManager.clearFocus() so any pending composition commits synchronously before the action dispatches.

2. Suspend getString in non-coroutine scope (same fix as #577). Main is currently broken because #576 merged without a compile pass — getString(Res.string.proxy_host_*) was called outside the surrounding viewModelScope.launch. Moved the calls inside; pass isBlank boolean across the boundary. Also missing import for proxy_host_invalid added.

Verified with ./gradlew :feature:tweaks:presentation:compileDebugKotlinAndroid :feature:tweaks:presentation:compileKotlinJvm :composeApp:compileDebugKotlinAndroid — all green.

If this lands first, #577 should be closed as redundant (its fix is duplicated here).

Summary by CodeRabbit

  • Bug Fixes
    • Software keyboard now hides correctly when testing or saving proxy settings.
    • Proxy validation error messages are consistent across actions.
    • Test button remains available during proxy configuration while tests run.
  • Stability
    • Update flows now propagate coroutine cancellations correctly to avoid suppressed cancellations.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2557981a-3da2-4374-bc16-3f87cafb4538

📥 Commits

Reviewing files that changed from the base of the PR and between 1ae7b14 and 3765b48.

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

Walkthrough

Proxy host validation now consistently selects between "required" and "invalid" messages; proxy Test/Save handlers hide the software keyboard and clear focus before calling the VM; the Test button is enabled when not testing. Separately, app update flows now rethrow CancellationException to allow normal coroutine cancellation.

Changes

Proxy Validation & Keyboard UX

Layer / File(s) Summary
Proxy Host Validation Messaging
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt
Imports proxy_host_invalid and refactors OnProxySave and buildProxyConfigForTest to compute isBlank from form.host and choose proxy_host_required vs proxy_host_invalid before emitting errors.
Keyboard Dismissal & Button Behavior
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt
Adds LocalSoftwareKeyboardController usage in ProxyDetailsFields, calls keyboardController?.hide() before focusManager.clearFocus() in Test/Save handlers, and enables Test when not in progress (validation remains in VM).

App Updates Cancellation Handling

Layer / File(s) Summary
Cancellation Rethrow
feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt
checkAllForUpdates() and refresh() now catch CancellationException separately and rethrow it, preserving existing generic exception logging and finally-based state resets.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I nudge the keyboard out of sight,
Hosts named wrong or missing get light,
Test hums on when work's not begun,
Save clears focus — the UX is fun.
Cancellations now hop away at night.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: fixing single-tap proxy save, addressing suspend getString issue, and preserving cancellation exception handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 fix/proxy-save-doubletap

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.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 11, 2026

Greptile Summary

  • Fixes a compile break introduced by fix(tweaks): proxy host validation + save-needs-two-taps #576 where getString() (a suspend function) was called outside a coroutine scope in TweaksViewModel; the fix captures the isBlank flag before the launch and moves the getString calls inside, restoring compilability on both Android and JVM targets.
  • Removes the isFormValid gate on the Save and Test buttons in Network.kt, replacing it with !form.isTestInProgress only, and adds keyboardController?.hide() before focusManager.clearFocus() to force IME composition-commit before the VM reads form state.
  • Adds correct CancellationException re-throw in two try/catch blocks in AppsViewModel to preserve structured concurrency semantics.

Confidence Score: 5/5

Safe to merge — all changes are targeted bug fixes with no new logic errors introduced.

Only pre-existing P2 finding (dead isFormValid variable) remains and was already flagged in the prior review. Both compile fixes are structurally correct and all three changed files pass their scoped compile targets per the PR description.

Network.kt still contains the dead isFormValid computation noted in the previous review, but it is a P2 style concern and does not block merging.

Important Files Changed

Filename Overview
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt Moves getString suspend calls inside viewModelScope.launch in both the save and test validation paths; adds missing proxy_host_invalid import. Fix is correct and consistent across both call sites.
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/components/sections/Network.kt Adds keyboardController?.hide() before clearFocus() on both buttons and drops the isFormValid gate; isFormValid is now dead code computed on every recomposition — flagged in previous review.
feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt Adds explicit CancellationException re-throw in both checkAllForUpdates and refresh try/catch blocks, correctly restoring structured concurrency invariants.

Sequence Diagram

sequenceDiagram
    participant User
    participant IME as Android IME
    participant Network as Network.kt (Composable)
    participant FocusManager
    participant KeyboardController
    participant TweaksVM as TweaksViewModel

    User->>Network: Tap Save / Test button
    Network->>KeyboardController: hide() — flush composition buffer
    Network->>FocusManager: clearFocus()
    Network->>TweaksVM: OnProxySave / OnProxyTest action

    alt host blank or invalid
        TweaksVM->>TweaksVM: "capture isBlank = form.host.isBlank()"
        TweaksVM->>TweaksVM: viewModelScope.launch
        TweaksVM->>TweaksVM: getString(proxy_host_required / proxy_host_invalid)
        TweaksVM-->>Network: _events.send(OnProxySaveError / OnProxyTestError)
    else port invalid
        TweaksVM->>TweaksVM: viewModelScope.launch
        TweaksVM->>TweaksVM: getString(invalid_proxy_port)
        TweaksVM-->>Network: _events.send(error event)
    else form valid
        TweaksVM->>TweaksVM: build ProxyConfig
        TweaksVM->>TweaksVM: viewModelScope.launch → save / test
        TweaksVM-->>Network: _events.send(OnProxySaved / test outcome)
    end
Loading

Reviews (2): Last reviewed commit: "fix(apps): preserve coroutine cancellati..." | Re-trigger Greptile

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.

🧹 Nitpick comments (1)
feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt (1)

639-647: ⚡ Quick win

Centralize the proxy-host error resource selection.

The blank-vs-invalid branch is now duplicated in both save and test validation paths. That logic already drifted once; keeping it in one helper will make the next validation tweak much safer.

♻️ Small extraction
+private fun proxyHostErrorRes(host: String) =
+    if (host.isBlank()) {
+        Res.string.proxy_host_required
+    } else {
+        Res.string.proxy_host_invalid
+    }
...
-                                        val isBlank = form.host.isBlank()
                                         viewModelScope.launch {
-                                            val msg =
-                                                if (isBlank) {
-                                                    getString(Res.string.proxy_host_required)
-                                                } else {
-                                                    getString(Res.string.proxy_host_invalid)
-                                                }
+                                            val msg = getString(proxyHostErrorRes(form.host))
                                             _events.send(TweaksEvent.OnProxySaveError(msg))
                                         }
...
-                            val isBlank = form.host.isBlank()
                             viewModelScope.launch {
-                                val msg =
-                                    if (isBlank) {
-                                        getString(Res.string.proxy_host_required)
-                                    } else {
-                                        getString(Res.string.proxy_host_invalid)
-                                    }
+                                val msg = getString(proxyHostErrorRes(form.host))
                                 _events.send(TweaksEvent.OnProxyTestError(msg))
                             }

Also applies to: 1026-1034

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`
around lines 639 - 647, Extract the blank-vs-invalid selection into a single
helper on TweaksViewModel (e.g., proxyHostErrorMessage(host: String): String)
that checks form.host.isBlank() and returns
getString(Res.string.proxy_host_required) or
getString(Res.string.proxy_host_invalid); then replace the duplicated branches
that build msg (the block that sends
_events.send(TweaksEvent.OnProxySaveError(msg)) and the similar code at the
other occurrence) to call this helper so both save and test validation use the
same logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@feature/tweaks/presentation/src/commonMain/kotlin/zed/rainxch/tweaks/presentation/TweaksViewModel.kt`:
- Around line 639-647: Extract the blank-vs-invalid selection into a single
helper on TweaksViewModel (e.g., proxyHostErrorMessage(host: String): String)
that checks form.host.isBlank() and returns
getString(Res.string.proxy_host_required) or
getString(Res.string.proxy_host_invalid); then replace the duplicated branches
that build msg (the block that sends
_events.send(TweaksEvent.OnProxySaveError(msg)) and the similar code at the
other occurrence) to call this helper so both save and test validation use the
same logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 37e08d7c-c423-4ec2-b7b4-93a61d2ec2fc

📥 Commits

Reviewing files that changed from the base of the PR and between 3ffa72f and 1ae7b14.

📒 Files selected for processing (2)
  • 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

@rainxchzed rainxchzed changed the title fix(tweaks): single-tap save proxy + suspend getString fix fix: single-tap save proxy + suspend getString + preserve cancellation May 11, 2026
@rainxchzed rainxchzed merged commit 7a503ee into main May 12, 2026
1 check passed
@rainxchzed rainxchzed deleted the fix/proxy-save-doubletap branch May 12, 2026 01:04
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