Skip to content

Keep Gmail actions when hiding or switching services - #37

Open
jpagh wants to merge 3 commits into
nicojan:mainfrom
jpagh:fix/gmail-sticky-actions
Open

jpagh wants to merge 3 commits into
nicojan:mainfrom
jpagh:fix/gmail-sticky-actions

Conversation

@jpagh

@jpagh jpagh commented Oct 3, 2026 •

Copy link
Copy Markdown

Gmail's hover buttons sometimes did nothing when you hid Chorus or switched services right after clicking: mark read, mark unread, archive, delete. Two things caused it.

Hide and switch both cleared the page's hover state too early. Clearing hover fires mouseout, and when that exit arrived before the click's request left the page, Gmail dropped the queued write. Chorus now lets the click finish first, then clears hover, and leaves a short window afterwards for the saves that mouseout itself starts.

Switching had a second problem. It showed the new service and tore the old page down in the same step, taking any request still in flight with it. The old page now stays attached underneath until its work has finished, and comes off once it goes quiet. The same holds when you switch to a Mac-app service, where the page used to be dropped before it could finish.

Hiding does the same work in the background. The window disappears at once, and a task keeps the app awake just long enough to settle before clearing hover.

Chorus also tells Gmail it is Safari 27 rather than 26.

What this does not fix

Quitting. A live check on the clean build failed twice: the phone never showed the change while Chorus was closed, so Gmail's servers never got it. Gmail holds the request back while the page has no focus, and nothing in Chorus changes that. Reloading in the same instant can lose the change for the same reason. The changelog says both.

Tests

Fifteen tests drive a real WKWebView through the hide, switch, reload, quit and native-panel paths, and cover quirks mode, a cancelled reload, focus, resize, quick return and empty selection. One more pins the two web-view construction paths together, and one covers the settle arithmetic. The suite is 333 tests, green on macOS 14 and 15 at e82cf65.

@jpagh jpagh changed the title Make Gmail actions survive hide, switch, and quit Keep Gmail actions when hiding, switching, or quitting Oct 3, 2026
jpagh added 3 commits October 5, 2026 11:38
Hiding Chorus, switching services, or quitting straight after a Gmail
hover action (mark read/unread, archive, delete) could lose it. Two
causes, both addressed:

- Chorus sent a synthetic hover-exit before giving the click's request
  time to leave. When the exit landed first, Gmail could drop the still
  queued write. Departure now waits out the click's settle window first
  and clears hover afterwards, with a short window left for saves the
  exit itself triggers.
- Hiding waited over two seconds and still guarded nothing; it is
  instant again. A background task waits out recent work under a
  wakefulness hold and clears hover after, and a service switch shows
  the new page at once while the old one settles underneath.

Reloading the very instant after a click stays best-effort: when Gmail
has not sent the change yet there is nothing to keep. Also presents
the Safari 27 user agent token to Gmail instead of 26.
Keep the web host attached beneath native and empty panels while the outgoing page settles. The native selection regression reproduced the early-detach failure before the fix; quick return cancels departure and retained pages cannot receive input.

Share foreground and preload construction, and replace the unsupported Gmail quit promise with a bounded-grace description. Debug suite: 324 tests, one expected skip, no failures. Live Gmail native-switch and quit acceptance remain pending.
The live acceptance run failed the immediate-quit case twice: the phone never showed the change while Chorus was closed, so the write never reached Gmail. The earlier wording only warned about reloading. One word makes the entry match what was measured.

humanizer_check_text (local run of nicojan/humanizer-mcp run_checks) on the Unreleased entry with content_type=notes: prohibitions_clear=true, no hard violations, no must_clear findings.
@jpagh
jpagh force-pushed the fix/gmail-sticky-actions branch from c83cac9 to e82cf65 Compare October 5, 2026 16:42
@jpagh jpagh changed the title Keep Gmail actions when hiding, switching, or quitting Keep Gmail actions when hiding or switching services Oct 5, 2026

@nicojan nicojan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks, Jack. You found the Gmail cause, and the hover timing makes sense to me. The trouble is that the fix now runs on every switch, reload and quit for every service, and that brings some regressions. I'd like it narrowed before it goes in.

Needs fixing

  1. The tab strip covers the page. In WebContentCard, WebViewContainer now fills the whole ZStack, and the VStack with ServiceTabStrip sits on top of it. On main the strip pushed the page down. Open a window.open tab (Canva or Figma) and the strip, which has no background, hides the top 32pt of the page and takes its clicks. Every new test passes tabs: nil, so none of them catch it. Please keep the strip above the host, and add a test with tabs open that checks the page's frame.
  2. Reload waits about two seconds. Clicking the toolbar button counts as input, so reload waits out the whole 2.2s settle window, though the pointer sits on the button and the page has no hover to clear. The spinner doesn't start during the wait, a second click queues a second reload, and the webView.url == originalURL guard drops the reload if the page rewrites its own URL meanwhile, which Gmail does. Please reload at once unless the pointer is over the page, and drop the URL guard.
  3. Please check fullscreen video by hand. WebViewContainer now re-attaches the page whenever webView.superview !== self. As far as I know, WebKit moves the WKWebView into its own window for element fullscreen, so a re-render during YouTube or Meet fullscreen may pull it back. I haven't confirmed this. Re-attaching only when the page has no superview, or belongs to another host, would avoid it.

Worth changing

  1. A click on the rail has already taken the pointer off the page, so WebKit has sent its own exit and there is nothing to wait for. Yet the old page stays attached for about 2.9s on every switch, and it still gets mouse moves and cursor updates in that time, because tracking areas don't care what's on top. Holding the page only when the pointer is over it would cut the change down to the case Gmail needs.
  2. If someone hides, comes back within 2.2s and clicks a Gmail action, the delayed exit from the hide task lands on that click. Skipping endHover when NSApp.isHidden is false fixes it.
  3. Quit now waits out the settle window first, so a click-driven quit (Sparkle's "Install and Relaunch") takes about three seconds longer. .userInitiatedAllowingIdleSystemSleep is enough for the activity.
  4. The CHANGELOG says Chorus tells Gmail it is Safari 27. safariDefault goes to every service without its own agent. Please put the user agent change in its own commit, say what it applies to, and drop the comment claiming Gmail treats 27 better unless you have measured it.
  5. Tests: none of them drive the wait-for-click path, since synthetic events skip the NSEvent monitor. InputSettle.shared carries state from one test to the next, and the time budgets are tight for the macOS 14 VM. I'll approve the CI run once you push.

@jpagh

jpagh commented Oct 6, 2026

Copy link
Copy Markdown
Author

Thanks. I agree. I'm definitely not thrilled with this current state either. I did actually test the Safari 27 user agent and Gmail responded differently with it, but I can't recall what improved now. Gmail is very poorly behaved and quite frustrating to even be able to reliably test or measure. I'll see what I can do.

This branch has not been deployed

No deployments
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