Skip to content

fix: close UX/robustness gaps found in codebase audit - #548

Closed
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:fix/bug-hunt-ux
Closed

fix: close UX/robustness gaps found in codebase audit#548
PierrunoYT wants to merge 2 commits into
Gitlawb:mainfrom
PierrunoYT:fix/bug-hunt-ux

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #546. Split out of #481 per review feedback, grouping the UX/robustness findings from #480 separately from the security-sensitive ones (see #547 for those).

  • cli: firstUsableProvider ignored OAuth logins when picking a fallback provider, forcing unnecessary re-onboarding for an already-authenticated user
  • sessions: exec prompt summarization truncated on a raw byte offset, which could split a multi-byte UTF-8 rune and embed invalid UTF-8 into a resumed session's prompt
  • tui: configurable keybindings had no collision detection, so a remapped chord could silently make a hardcoded shortcut (or another configured binding) permanently unreachable
  • update: replaceBinary's restore-on-failure rename had no retry, risking a permanently missing binary on a transient Windows file lock

Dropped from this split: the ninth original finding (Windows shell-syntax pre-flight check not being quote-aware, internal/tools/shell_runtime.go) needs no further action — #476 landed on main independently since #481 was opened and already replaced that check with a more thorough segment/word-anchored implementation that resolves the same false-positive.

Test plan

  • go build ./... clean
  • Regression test added for each fix
  • go test ./internal/cli/... ./internal/sessions/... ./internal/tui/... ./internal/update/... passes

Summary by CodeRabbit

  • Bug Fixes
    • Improved onboarding fallback so stored OAuth logins are recognized and won’t trigger repeated prompts when access is already authenticated.
    • Fixed prompt/session text truncation to respect UTF-8 rune boundaries, preventing mid-character cuts.
    • Improved keybinding sanitization to detect shortcut collisions, reset conflicting bindings, and display warning notices consistently.
    • Enhanced Windows executable replacement reliability by retrying renames to reduce failures from transient file locks.

Split out of Gitlawb#481 per review feedback, grouping the UX/robustness
findings from issue Gitlawb#480 (a multi-agent codebase audit) separately
from the security-sensitive ones:

- cli: firstUsableProvider ignored OAuth logins when picking a
  fallback provider, forcing unnecessary re-onboarding for an
  already-authenticated user
- sessions: exec prompt summarization truncated on a raw byte offset,
  which could split a multi-byte UTF-8 rune and embed invalid UTF-8
  into a resumed session's prompt
- tui: configurable keybindings had no collision detection, so a
  remapped chord could silently make a hardcoded shortcut (or another
  configured binding) permanently unreachable
- update: replaceBinary's restore-on-failure rename had no retry,
  risking a permanently missing binary on a transient Windows file
  lock

Note: the ninth original finding (the Windows shell-syntax pre-flight
check not being quote-aware, internal/tools/shell_runtime.go) is
dropped from this split entirely -- Gitlawb#476 landed on main independently
and already replaced that check with a more thorough segment/word-
anchored implementation that resolves the same false-positive.

All fixes ship with regression tests. Refs Gitlawb#480.
Copilot AI review requested due to automatic review settings July 6, 2026 08:42

Copilot AI left a comment

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.

Pull request overview

This PR addresses four UX/robustness issues from the codebase audit (#546): making provider fallback respect stored OAuth logins, ensuring session prompt summarization doesn’t corrupt UTF-8, preventing silent TUI keybinding collisions, and improving Windows self-update robustness by retrying restore renames.

Changes:

  • CLI: treat stored OAuth logins as usable credentials when selecting a fallback provider.
  • Sessions: truncate exec prompt summaries on UTF-8 rune boundaries (not raw byte offsets) and add regression coverage.
  • TUI + Update: sanitize user keybindings to avoid collisions and add a Windows rename-restore retry path (with tests).

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/cli/setup.go Includes OAuth login state in firstUsableProvider fallback selection.
internal/cli/setup_fallback_test.go Adds regression test ensuring OAuth-only providers can be selected as fallback.
internal/sessions/exec_session.go Introduces UTF-8-safe truncation via truncateUTF8 for exec prompt summarization.
internal/sessions/store_test.go Adds regression test asserting summarizePayload returns valid UTF-8 and respects the 500-byte cap.
internal/tui/model.go Applies sanitized keybindings and surfaces collision warnings as startup notices.
internal/tui/keybindings.go Adds reserved-chord list and sanitizeKeyBindings collision detection/reversion logic.
internal/tui/binding_test.go Adds tests covering reserved collisions, mutual collisions, and collisions with other defaults.
internal/update/replace_windows.go Adds renameWithRetry for restore-on-failure robustness under transient Windows rename failures.
internal/update/replace_windows_test.go Adds Windows-only tests for replaceBinary and renameWithRetry.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/tui/keybindings.go Outdated
Comment on lines +343 to +358
claimedBy := map[parsedBinding]string{}
for _, e := range entries {
if e.binding.isZero() {
continue
}
if other, ok := claimedBy[*e.binding]; ok {
warnings = append(warnings, fmt.Sprintf(
"keybindings.%s (%s) conflicts with keybindings.%s; using the default instead.",
e.name, e.binding.Label(), other))
*e.binding = parsedBinding{}
continue
}
claimedBy[*e.binding] = e.name
}

return b, warnings
Comment thread internal/update/replace_windows.go Outdated
Comment on lines +41 to +48
for attempt := 0; attempt < restoreRenameRetryAttempts; attempt++ {
if err := os.Rename(oldPath, newPath); err == nil {
return nil
} else {
lastErr = err
}
time.Sleep(restoreRenameRetryDelay)
}
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR updates provider fallback to recognize stored OAuth logins, sanitizes TUI key bindings for collisions and reserved chords, truncates session payload summaries on UTF-8 rune boundaries, and retries Windows binary renames during replacement. Each area includes added or updated tests.

Changes

OAuth-login-aware provider fallback

Layer / File(s) Summary
Skip onboarding when OAuth login exists
internal/cli/setup.go, internal/cli/setup_fallback_test.go
firstUsableProvider now considers stored OAuth logins when skipping missing-credential onboarding, and a new test seeds an OAuth token store to verify provider selection.

Key binding collision sanitization

Layer / File(s) Summary
sanitizeKeyBindings logic and reserved chords
internal/tui/keybindings.go
Adds reservedBindings and sanitizeKeyBindings to clear colliding configurable bindings and emit warnings.
Model wiring and warning display
internal/tui/model.go
newModel resolves and sanitizes bindings, stores the sanitized result, and appends warnings to the transcript.
Sanitization collision tests
internal/tui/binding_test.go
Adds coverage for reserved, mutual, default, chained, bare-key, and non-collision binding cases.

UTF-8 safe payload truncation

Layer / File(s) Summary
truncateUTF8 helper and test
internal/sessions/exec_session.go, internal/sessions/store_test.go
summarizePayload now truncates at UTF-8 rune boundaries, and a test verifies the output stays valid UTF-8 within the size limit.

Windows binary restore retry

Layer / File(s) Summary
renameWithRetry helper and integration
internal/update/replace_windows.go, internal/update/replace_windows_test.go
Windows replacement now retries rename operations, reports restore failures alongside install failures, and adds tests for replacement, cleanup, and retry behavior.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Setup as firstUsableProvider
  participant OAuthStore as oauthLoggedInProviders
  participant Profile as Provider profile

  Setup->>OAuthStore: fetch stored OAuth logins
  OAuthStore-->>Setup: logins set
  Setup->>Profile: check setupMissingCredentialEnv
  alt credentials missing and no OAuth login
    Setup->>Setup: skip provider
  else OAuth login found or credentials present
    Setup-->>Setup: select provider as usable
  end
Loading

Possibly related issues

Possibly related PRs

  • Gitlawb/zero#366: Both PRs modify the CLI onboarding/auth-gating path to recognize stored OAuth credentials.
  • Gitlawb/zero#417: Both PRs touch the TUI keybinding pipeline in internal/tui.
  • Gitlawb/zero#461: Both PRs touch the Windows update binary replacement logic.

Suggested reviewers: Vasanthdev2004, anandh8x, gnanam1990, kevincodex1

🚥 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 matches the PR’s overall theme of fixing UX and robustness issues from the codebase audit, even though it is broad.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/update/replace_windows.go (1)

26-37: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider retrying the initial install rename too, not just the restore.

The comment attributes the failure to a transient Windows lock (AV/indexer scanning, lingering handle). That same transient condition is just as likely to hit the initial os.Rename(newPath, targetPath) as the restore rename, yet only the restore path gets retried. Wrapping the install rename with renameWithRetry as well would reduce spurious update failures without adding meaningfully more risk.

♻️ Symmetric retry on install
-	if err := os.Rename(newPath, targetPath); err != nil {
+	if err := renameWithRetry(newPath, targetPath); err != nil {
 		// Retry the restore: a transient Windows file lock (antivirus/indexer
🤖 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 `@internal/update/replace_windows.go` around lines 26 - 37, The initial install
rename in replaceWindowsBinary currently uses os.Rename(newPath, targetPath)
without retry, while only the restore path uses renameWithRetry. Update the
replaceWindowsBinary flow to wrap the first rename in renameWithRetry as well,
using the same retry behavior already applied for restoring oldPath, so
transient Windows file locks are handled symmetrically.
internal/update/replace_windows_test.go (1)

39-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test exercises replaceBinary's install-fails → restore-retry branch.

Existing tests cover renameWithRetry in isolation and replaceBinary's happy path, but nothing drives the actual combined-error path in replaceBinary (lines 26-37 of replace_windows.go) where the install rename fails and the restore is retried. A test that opens targetPath exclusively (denying rename) before calling replaceBinary, then asserts the error mentions both failures and that oldPath still holds the original bytes, would validate the new behavior end-to-end rather than just the retry primitive.

🤖 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 `@internal/update/replace_windows_test.go` around lines 39 - 65, The current
tests do not cover the combined failure path in replaceBinary where the install
rename fails and the restore is retried. Add an end-to-end test around
replaceBinary that holds targetPath open exclusively to force the initial rename
failure, then verify the returned error includes both the install and restore
failures. Also assert oldPath still contains the original bytes after the retry
path, using replaceBinary and renameWithRetry as the key symbols to locate the
behavior.
🤖 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.

Inline comments:
In `@internal/tui/keybindings.go`:
- Around line 296-359: The collision cleanup in sanitizeKeyBindings is
order-dependent, so chained conflicts like toggleMouse and togglePlan can still
end up sharing the same chord after one binding is reset to its default. Rework
sanitizeKeyBindings to resolve bindings to a fixed point (or precompute
effective chords before comparing) so every conflict is re-evaluated after any
default adoption. Make sure the final conflict pass considers bindings that were
just reset, not only non-zero entries, and add a regression test covering the
toggleMouse/togglePlan chained-default case.

In `@internal/update/replace_windows.go`:
- Around line 26-37: The rollback copy is being lost because
CleanupStaleBinary(executablePath) can run after a failed replace and delete the
only preserved <binary>.old backup. Update Apply and the Windows replace flow in
replace_windows.go so stale-binary cleanup only happens when targetPath still
exists, or move the cleanup to after a successful install path. Use the existing
replaceWithRetry/renameWithRetry logic as the place to preserve the backup until
the replacement succeeds.

---

Nitpick comments:
In `@internal/update/replace_windows_test.go`:
- Around line 39-65: The current tests do not cover the combined failure path in
replaceBinary where the install rename fails and the restore is retried. Add an
end-to-end test around replaceBinary that holds targetPath open exclusively to
force the initial rename failure, then verify the returned error includes both
the install and restore failures. Also assert oldPath still contains the
original bytes after the retry path, using replaceBinary and renameWithRetry as
the key symbols to locate the behavior.

In `@internal/update/replace_windows.go`:
- Around line 26-37: The initial install rename in replaceWindowsBinary
currently uses os.Rename(newPath, targetPath) without retry, while only the
restore path uses renameWithRetry. Update the replaceWindowsBinary flow to wrap
the first rename in renameWithRetry as well, using the same retry behavior
already applied for restoring oldPath, so transient Windows file locks are
handled symmetrically.
🪄 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: 0aeacfb9-ab92-4045-abf3-8f9c7e5b2bb0

📥 Commits

Reviewing files that changed from the base of the PR and between fd69233 and b9c33cd.

📒 Files selected for processing (9)
  • internal/cli/setup.go
  • internal/cli/setup_fallback_test.go
  • internal/sessions/exec_session.go
  • internal/sessions/store_test.go
  • internal/tui/binding_test.go
  • internal/tui/keybindings.go
  • internal/tui/model.go
  • internal/update/replace_windows.go
  • internal/update/replace_windows_test.go

Comment thread internal/tui/keybindings.go
Comment thread internal/update/replace_windows.go Outdated
- tui: sanitizeKeyBindings' collision passes were order-dependent, so a
  reversion made mid-pass (e.g. togglePlan reverting to its own default
  of ctrl+p because ctrl+t collided with cycleReasoning's default)
  could newly collide with an entry already checked earlier in that
  same pass (toggleMouse's explicit ctrl+p), leaving both bound to the
  same chord undetected. Loop all three collision checks to a fixed
  point instead of running each once. Regression test added for the
  toggleMouse/togglePlan chained case.

- update: CleanupStaleBinary unconditionally removed "<binary>.old" on
  every Apply() call. If a previous replaceBinary call failed to both
  install the new binary and restore the original, targetPath is left
  missing and oldPath is the only surviving working copy -- the next
  Apply() would delete it before the caller has any chance to recover.
  Guard cleanup on targetPath still existing.

- update: replaceBinary only retried the restore-on-failure rename,
  not the initial install rename, though both are subject to the same
  transient Windows file lock (antivirus/indexer). Retry both via
  renameWithRetry. Also stopped renameWithRetry from sleeping after
  its final (already-failed) attempt, and dropped the needless
  else block CodeRabbit flagged alongside it.

Regression tests added for the CleanupStaleBinary guard and the
install-fails/restore-succeeds branch of replaceBinary. Skipped the
suggested test for the double-failure (install AND restore both fail)
branch: reliably forcing two independent os.Rename calls to fail
in-process without a flaky low-level Windows locking hack isn't worth
the fragility for a nitpick-level ask.
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Addressed in 86125b7:

  • Major (keybindings.go): confirmed — toggleMouse="ctrl+p" / togglePlan="ctrl+t" did end up on the same chord undetected, because togglePlan's mid-pass reversion to its own default (ctrl+p) was never re-checked against toggleMouse, which had already been processed earlier in the same pass. sanitizeKeyBindings now loops all three collision checks to a fixed point. Added TestSanitizeKeyBindingsResolvesChainedDefaultCollision covering exactly this case.
  • Major (replace_windows.go): confirmed — Apply() calls CleanupStaleBinary unconditionally on every run, so if a previous replaceBinary failed at both the install and the restore, the next Apply() would delete the only surviving .old backup before anyone could recover it. CleanupStaleBinary now no-ops when targetPath doesn't exist. Added tests for both branches.
  • Nitpick (symmetric retry): applied — the initial install rename now goes through renameWithRetry too, not just the restore.
  • Nitpick (renameWithRetry sleeps after final attempt): applied — no more sleep after the last (already-failed) attempt, and dropped the redundant else.
  • Nitpick (missing install-fails/restore-retry test): partially addressed. Added TestReplaceBinaryRestoresOriginalWhenInstallFails covering install-fails→restore-succeeds. Skipped the double-failure (both renames fail) branch — reliably forcing two independent os.Rename calls to fail in-process on Windows without a flaky low-level locking hack isn't worth it for a nitpick-level ask; happy to revisit if you feel strongly.

go build ./... clean, go vet ./... clean, full package tests pass on Windows.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/update/replace_windows_test.go (1)

43-59: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid real retry sleeps in this test.
replaceBinary will spend ~900ms in renameWithRetry backoff here (9 sleeps × 100ms) before it even reaches the restore path, so this adds a real delay to every test run. Inject the delay/backoff for tests or make the retry wait overrideable.

🤖 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 `@internal/update/replace_windows_test.go` around lines 43 - 59, The
replaceBinary test is incurring real backoff time because renameWithRetry uses
fixed retry sleeps before the restore path is reached. Update
replaceBinary/renameWithRetry so the retry delay is injectable or overrideable
in tests, and adjust TestReplaceBinaryRestoresOriginalWhenInstallFails to use
the fast test delay instead of waiting on the default sleep behavior.
🤖 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 `@internal/update/replace_windows_test.go`:
- Around line 43-59: The replaceBinary test is incurring real backoff time
because renameWithRetry uses fixed retry sleeps before the restore path is
reached. Update replaceBinary/renameWithRetry so the retry delay is injectable
or overrideable in tests, and adjust
TestReplaceBinaryRestoresOriginalWhenInstallFails to use the fast test delay
instead of waiting on the default sleep behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 611effb0-166f-437b-a92d-1ea13297fa03

📥 Commits

Reviewing files that changed from the base of the PR and between b9c33cd and 86125b7.

📒 Files selected for processing (4)
  • internal/tui/binding_test.go
  • internal/tui/keybindings.go
  • internal/update/replace_windows.go
  • internal/update/replace_windows_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/tui/binding_test.go
  • internal/tui/keybindings.go
  • internal/update/replace_windows.go

@PierrunoYT PierrunoYT closed this Jul 6, 2026
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.

UX/robustness fixes from the codebase audit (#480): OAuth fallback, UTF-8 truncation, keybinding collisions, Windows rename retry

2 participants