Skip to content

Keep rapid Tab accepts owned while the overlay holds its next present - #838

Merged
FuJacob merged 1 commit into
mainfrom
fix/rapid-tab-acceptance
Sep 29, 2026
Merged

FuJacob merged 1 commit into
mainfrom
fix/rapid-tab-acceptance

Conversation

@FuJacob

@FuJacob FuJacob commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Summary

Regression from #827: OverlayController.showSuggestion can now hold a presentation (caret-lag hold, or while the pixel caret locator reads the host). During that hold overlayState still shows the previous ghost, so a rapid second Tab failed SuggestionSessionReconciler.overlayAllowsAcceptance (visible text ≠ remaining tail), the coordinator called passTabThrough, the session was cleared, and Tab reached the host — moving focus to page buttons in browsers. The overlay now exposes heldPresentationText, and the acceptance rule also authorizes the tail when it is exactly the text the overlay is about to paint. A stale visible ghost with nothing held still passes Tab through as before.

Validation

  • Full unit suite (CODE_SIGNING_ALLOWED=NO, build/DerivedData): 2642 tests, 0 failures, 16 skipped.
  • New test_rapidTabsAcceptEachWordWhileTheOverlayIsStillHoldingThePreviousPresent reproduces the leak with the rule disabled (second Tab not consumed, only " hello" inserted) and passes with the fix; plus held-present / stale-ghost coordinator tests, an interaction-state test, and a pure-rule test.
  • Not yet verified live; suggested check: Cotabby Dev with -cotabby-debug, mash Tab through a multi-word suggestion in a Chrome composer and in Obsidian, and confirm no tab-passed-through follows pixel-caret-hold / caret-lag-hold.

Linked issues

Refs #827

Risk / rollout notes

  • SuggestionOverlayControlling gains heldPresentationText with a nil default, so other conformers are unaffected.
  • Possible remaining cause not addressed here: Integrate autocomplete improvements while preserving upstream lifecycle #832 tightened same-field identity (focus sequence, title, URL, placeholder) and made every hideOverlay end the post-exhaustion window; if live logs show reconcile-mismatch / "focused field changed" before a pass-through, that needs a follow-up.

Summary by CodeRabbit

  • Bug Fixes
    • Rapid successive Tab presses can now accept the next word even while the suggestion display is catching up, instead of passing Tab through to the host app.
    • Acceptance remains limited to text that matches the pending suggestion, preventing stale displayed text from being accepted.
    • The remaining suggestion text stays available for acceptance after the display updates.

RetriggerConfidence Score: 4/5

The PR appears safe to merge, with non-blocking production-overlay test coverage still worth adding.

Fix All in CodexFindings

  1. P2 Production hold paths lack coverage ▶

Summary

The PR lets acceptance recognize a suggestion tail whose next overlay presentation is still held, preventing a rapid follow-up Tab from passing through when the published overlay shows the previous tail.

  • Adds held-presentation state to the overlay contract and production controller.
  • Passes that state through coordinator validation and adds rapid-accept regression tests.
  • The new tests do not directly cover the production controller’s hold lifecycle.

Reviews (1) · Last reviewed commit: "Keep rapid Tab accepts owned while the o..."

The ghost-text rewrite in #827 lets OverlayController.showSuggestion return
without publishing state: it holds a presentation while a pixel caret read is
in flight (PixelCaretLocator, for union-run paragraphs and estimated
single-line carets) or while the host caret lags its published text
(CaretLagPolicy). During that hold `overlayState` still names the previous
presentation, and advanceInline refuses to slide while the panel is held, so
after a Tab accept the published overlay text is the pre-accept tail.

A rapid follow-up Tab then failed SuggestionSessionReconciler.
overlayAllowsAcceptance (visible text must equal the session tail),
passTabThrough cleared the session, and the accept tap returned the original
Tab to the host: the ghost vanished and browser focus jumped to the page's
next control. A slower Tab landed after the capture callback re-ran the
present and succeeded, which is why only fast Tabbing broke.

The controller now exposes the text it is holding (`heldPresentationText`),
and acceptance treats that in-flight present as offered: the visible ghost
must equal the tail, or the tail must be exactly what Cotabby is about to
paint. A mismatched ghost with nothing held is still stale UI and passes Tab
through as before.
Copilot AI balanced review requested due to automatic review settings September 29, 2026 00:43

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 249fc8d0-df05-4889-8a53-44eabb32aff3

📥 Commits

Reviewing files that changed from the base of the PR and between d0465e5 and f977296.

📒 Files selected for processing (9)
  • Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift
  • Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift
  • Cotabby/Services/Presentation/OverlayController.swift
  • Cotabby/Services/Suggestion/State/SuggestionInteractionState.swift
  • Cotabby/Support/Suggestion/Session/SuggestionSessionReconciler.swift
  • CotabbyTests/App/Coordinators/Suggestion/SuggestionCoordinatorAcceptanceTests.swift
  • CotabbyTests/Services/Suggestion/State/SuggestionInteractionStateTests.swift
  • CotabbyTests/Support/Suggestion/Acceptance/SuggestionOverlayAcceptanceTests.swift
  • CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

OverlayController now exposes text for a suggestion presentation that is pending. Acceptance preparation passes that text to session validation, which checks it alongside visible overlay text. Tests cover deferred presentations, rapid acceptance, and stale visible overlays.

Changes

Pending Presentation Acceptance

Layer / File(s) Summary
Track held overlay presentations
Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift, Cotabby/Services/Presentation/OverlayController.swift, CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift
The overlay contract exposes held presentation text. The controller sets it when a presentation is pending and clears it when a show is applied or the overlay is hidden. Test support can defer and apply presentations.
Validate acceptance against held text
Cotabby/App/Coordinators/Suggestion/SuggestionCoordinator+Acceptance.swift, Cotabby/Services/Suggestion/State/SuggestionInteractionState.swift, Cotabby/Support/Suggestion/Session/SuggestionSessionReconciler.swift, CotabbyTests/App/Coordinators/Suggestion/SuggestionCoordinatorAcceptanceTests.swift, CotabbyTests/Services/Suggestion/State/SuggestionInteractionStateTests.swift, CotabbyTests/Support/Suggestion/Acceptance/SuggestionOverlayAcceptanceTests.swift
Acceptance preparation passes held text to validation. A visible overlay allows acceptance when either visible or held text matches the requested text. Tests cover matching held text, successive acceptance, and rejection when only stale visible text is present.

Priority: ➖ Normal

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

Change: Bug fix

Suggested reviewers: mc-hamster

Merge Risk: ⚪ Minimal · up to f9772

No actionable merge-blocking issue was established. Rapid Tab acceptance still warrants the planned live Chrome and Obsidian checks.

Security Architecture Review

Security architecture risk: 🔵 Low · up to f9772

The new rule is limited to pending text that matches the active suggestion. Existing permission, focused-field, and session checks still precede insertion. Targeted tests cover rapid acceptance, but the behavior has not been verified in a live host.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The relevant sensitive outcome is insertion into the currently focused host editor. The changed match can make rapid acceptance reach that outcome, but the reviewed path retains focused-context validation rather than granting authority to overlay text alone.

Trust Boundaries and Controls

  • observed — A stale visible ghost without matching held text still fails the overlay check. Even when held text matches, validation requires an active session and checks the live focused context before acceptance is ready.

Resilience and Maintainability Implications

  • inferred — Cancellation safety depends on callers that abandon an offer also hiding or replacing its overlay; prediction-work cancellation does not invalidate the overlay's pending callback on its own. The inspected teardown and focus-change paths pair cancellation with hide, so this is a remaining coverage question rather than an established bypass.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 24 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preserving rapid Tab acceptance while the overlay holds the next presentation.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

Comment on lines +105 to 110
/// or a lagging host caret: it records the text as held and leaves `state` on the previous
/// presentation. `landHeldPresentation()` then applies it, as the capture callback would.
var defersPresentations = false
private(set) var heldPresentationText: String?
private var heldGeometry: SuggestionOverlayGeometry?

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.

P2 Production hold paths lack coverage The rapid-Tab tests defer presentations through this test rig, but the production controller holds presentations through a caret-lag check or a pixel-caret callback. The new tests therefore cannot catch a mistake in how those paths set or clear heldPresentationText, even if Tab still reaches the host. Please add focused tests for the production hold and clearing behavior.

Knowledge Base Used: Suggestion overlay presentation

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex Fix in Claude Code

@FuJacob
FuJacob merged commit 7724926 into main Sep 29, 2026
6 checks passed
@FuJacob
FuJacob deleted the fix/rapid-tab-acceptance branch September 29, 2026 00:51
flexi767 added a commit to flexi767/cotabby that referenced this pull request Sep 30, 2026
…anchor

Brings in FuJacob/cotabby through 7724926 (FuJacob#825, FuJacob#827, FuJacob#834, FuJacob#835, FuJacob#836, FuJacob#837,
FuJacob#838): ghost text matched to the host's own rendering plus finishing the word
being typed, Obsidian/Electron accessibility priming, rapid Tab accepts kept
owned while the overlay presents, Apple Intelligence greyed out when
unavailable, a new llama-recall eval fixture, the installer artwork and
kickstart script, and a CI test-flakiness fix.

Resolution, where both sides changed the same thing:
- Suppression reasons: upstream's `CompletionContentPolicy` rejects any
  completion without a letter or digit (`noWordContent`), which subsumes this
  fork's `periodOnly`, so that case and its helper are removed and the bare-dot
  test now asserts the broader reason. Behaviour change worth knowing: a lone
  closing bracket or question mark is now withheld too, where this fork
  deliberately let those through; its test records that.
- Prompt renderer: upstream's `personaLine(_:prefix:)` (the name only appears at
  a sign-off) wins; this fork's learned-phrases section is unchanged beside it.
- Eval case schema: upstream added `visualContextSummary` and `clipboardContext`
  for its recall fixture; this fork's `screenText` and `learnedPhrases` stay
  alongside them. The harness prefers the pre-rendered summary and still puts
  raw `screenText` through the live local passes. Both scorer helpers kept.
- Kept from this fork: the separate test host, the 2-4 word eval preset, phrase
  memory, the clock-time rule, the debug-artifact gate.

Full suite: 2,724 tests, 16 skipped, 0 failures; the installed app's defaults
byte-identical afterwards and no debug artifacts written.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants