Skip to content

fix(mship): return the chat connect flow to the tab that started it - #6403

Merged
waleedlatif1 merged 10 commits into
stagingfrom
fix/mship-return-to-same-tab
Aug 8, 2026
Merged

fix(mship): return the chat connect flow to the tab that started it#6403
waleedlatif1 merged 10 commits into
stagingfrom
fix/mship-return-to-same-tab

Conversation

@j15z

@j15z j15z commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Chat credential connects now run in a popup that returns through a new self-closing /oauth/chat-complete page, so the chat tab updates in place instead of the flow landing on a duplicate copy of the app in a new tab
  • The connect verdict now comes from the server routing the flow to its success callback, not from diffing the workspace credential list — that diff reported failure whenever you re-authorized an account you had already linked, since that path updates the account row and creates no new credential
  • The row's connected lock is stricter than its label: a swallowed credential-draft failure leaves the row retryable instead of latching it shut with nothing behind it
  • The popup is named per attempt, so sibling credential rows can't renavigate each other's window
  • A cross-origin connect URL stays on the anchor (keeping rel="noopener") instead of taking the popup path
  • The focus verifier reads the attempt after its refetch, so a verdict published mid-flight isn't overwritten, and it treats a popup parked on /oauth-error or the workspace error exit as finished rather than waiting on it

Desktop

Unaffected — the desktop app never reaches the popup path.

Desktop can't run OAuth in an embedded window: Better Auth binds the flow's state to the initiating browser's cookies, the Sim browser panel's partition isn't signed in to Sim, and Google/Microsoft reject embedded user agents outright. So the chip hands the whole flow to the system browser through the desktop bridge (beginOAuthConnect), which opens /desktop/connect, runs the authorize route there, and bounces /desktop/connect/complete to the app's 127.0.0.1 loopback. The app refocuses itself and refreshes credentials off that push.

Why this PR can't touch it: onConnectClick takes the bridge branch and returns before buildOAuthChatCompleteAuthorizeUrl or window.open is ever called, so popupRef is never set on desktop. Every guard added here keys off that ref — isPopupStillOpen(null) exits on its first line — which makes the terminal-path check and the per-attempt popup naming unreachable. Completion still arrives through useDesktopOAuthConnectListener in use-oauth-return.ts, untouched by this PR, and desktop keeps its own attempt correlation via setActiveDesktopOAuthChatAttempt / resolveDesktopOAuthChatAttempt.

Type of Change

  • Bug fix

Testing

Tested manually. bun run lint, check:audits (22 audits), check:api-validation, and type-check all pass. 173 test files / 1800 tests green, including new coverage for the close-refused redirect and its cross-origin returnTo rejection, the per-attempt popup name, the terminal-page settle, and the retryable-lock behavior.

Not verified in a real browser: whether COOP: same-origin on /oauth/chat-complete ever refuses window.close(). It closed fine in manual testing; the 400ms redirect is the fallback if it doesn't.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Connecting an integration from a chat credential chip opened OAuth in a
new tab and returned there, so the user landed on a second copy of the
app while the conversation they started from sat stale behind it.

The flow now runs in a popup and returns through a new self-closing page
at /oauth/chat-complete, which publishes its verdict to the shared
attempt record and closes. The chat tab picks that up over its storage
listener and updates in place, so it never navigates. A blocked popup
takes the same route in a new tab and still lands on the completion
page, so both paths share one verdict source.

That verdict is now the server's: reaching the completion page means
Better Auth routed the flow to its success callback. The previous check
diffed the workspace credential list, which reported failure whenever a
user re-authorized an account they had already linked -- that path
updates the account row and creates no new credential.

The lock is stricter than the label. A failure to create the credential
from its draft is swallowed server-side, so a flow can report success
with nothing in the workspace; the row stays retryable unless the
credential actually appears.

Also: the popup is named per attempt so sibling rows cannot renavigate
each other's window; a cross-origin connect URL keeps the anchor's
noopener instead of taking the popup path; the focus verifier reads the
attempt after its refetch so a verdict published mid-flight is not
overwritten; and the verifier treats a popup parked on a terminal page
(/oauth-error, the workspace error exit) as finished rather than
waiting on it forever.
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 8, 2026 6:32am

Request Review

@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches OAuth return routing, localStorage verdict correlation, and COOP header exceptions—security-sensitive auth UX with many edge cases, though same-origin checks and desktop bypass limit blast radius.

Overview
Chat credential Connect links now open OAuth in a popup that returns through a new /oauth/chat-complete page instead of navigating the chat tab away. That page writes the attempt verdict to localStorage (success vs error query) and closes the window, with a same-origin returnTo redirect if close fails.

buildOAuthChatCompleteAuthorizeUrl rewrites authorize callbacks to that completion route; cross-origin authorize URLs stay on the anchor with noopener. useOAuthChipConnection adds popup polling, terminal paths (/oauth-error, /workspace), COOP-safe observation, per-attempt window names, focus/refetch settlement, and a 10‑minute backstop. The row can show Connected from the verdict but only locks once credentials corroborate (reconnect exempt). next.config exempts OAuth popup routes from strict COOP so the opener can observe and close the popup.

Reviewed by Cursor Bugbot for commit f41e66c. Configure here.

@greptile-apps

greptile-apps Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR moves chat credential OAuth into per-attempt popups and routes successful returns through a self-closing completion page, preserving the initiating chat tab and its in-progress state.

  • Persists and publishes server-derived OAuth attempt verdicts through /oauth/chat-complete.
  • Adds popup polling, terminal-page detection, retry protection, and a bounded fallback for unobservable windows.
  • Separates the connected label from the credential-backed row lock and adds focused regression coverage.
  • Adjusts opener-policy route headers so OAuth completion and error pages remain observable by the initiating workspace.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/use-oauth-chip-connection.ts Introduces the popup lifecycle, attempt-correlated completion handling, retry protection, credential corroboration, and bounded settlement without leaving a blocking prior issue outstanding.
apps/sim/lib/credentials/oauth-chat-attempt.ts Adds safe completion-URL construction and makes attempt persistence failures non-throwing so the handoff can still release its window.
apps/sim/app/oauth/chat-complete/chat-complete-handoff.tsx Publishes the callback verdict, closes the OAuth window, and uses a sanitized same-origin fallback when closure is refused.
apps/sim/next.config.ts Extends permissive opener-policy routing to the OAuth completion and error pages needed by the popup lifecycle.
apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/special-tags.test.tsx Adds regression coverage for popup ownership, completion, terminal settlement, remount deadlines, retry behavior, and credential-backed locking.

Sequence Diagram

sequenceDiagram
  participant Chat as Chat tab
  participant Popup as OAuth popup
  participant Provider as OAuth provider
  participant Complete as /oauth/chat-complete
  Chat->>Chat: Create pending attempt
  Chat->>Popup: Open per-attempt authorize URL
  Popup->>Provider: Run authorization
  Provider-->>Complete: Return with attempt identifier
  Complete->>Chat: Publish verdict through shared attempt storage/event
  Complete->>Popup: Request window.close()
  Chat->>Chat: Refetch credentials and update row
  alt Popup cannot close
    Complete->>Complete: Redirect to sanitized same-origin returnTo
  end
Loading

Reviews (8): Last reviewed commit: "chore(mship): tighten the comments on th..." | Re-trigger Greptile

Comment thread apps/sim/lib/credentials/oauth-chat-attempt.ts Dismissed
Comment thread apps/sim/lib/credentials/oauth-chat-attempt.ts Dismissed
Addresses the review findings on the chat OAuth return leg.

- Watch the popup on an interval. A provider interstitial bouncing to the
  workspace root, a denied consent on /oauth-error, or a closed window all end
  the flow without publishing a verdict or firing any event in this tab, so the
  row waited forever. The focus handler also no longer consumes the away flag
  when it defers to a live popup.
- Focus an already-running popup on a repeat click instead of starting a rival
  attempt, which orphaned the first flow's verdict on an attempt id the row had
  stopped reading.
- Settle from the refetched credentials on the popup success path, so the row's
  lock is corroborated and a connected row stops being clickable.

Extracts the shared refetch-then-decide step into settleFromCredentials, used
by the focus handler, the popup watcher, and the success path.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

A provider page with COOP same-origin disowns the popup, and the disowned
handle reports closed for a consent screen still running. The watcher took
that as an ending and published 'failed' against a live flow.

- Replace the boolean with a three-state observation. Only a same-origin
  terminal page counts as 'ended'; a closed-or-disowned handle is
  'unobservable' and publishes no verdict. Closing a popup hands focus back
  to this tab anyway, so the focus verification settles that case.
- Stop the interval before settling. The refetch leaves the status pending
  for its duration, so a running interval could fire again and resolve an
  attempt a retry had since replaced.
- Gate the success toast on a launched-attempt ref rather than the window
  handle, which the watcher clears before React applies the verdict.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

… context

/oauth/chat-complete runs as a popup but fell into the strict COOP rule, so
same-origin moved it into its own browsing-context group the moment it loaded
— disowning it from the tab that opened it. That is the documented cause of a
popup that is not reliably script-closable and whose opener sees window.closed
report true for a live window.

Matches it to its opener's same-origin-allow-popups instead, which is the
directive the platform provides for exactly this case.
… opener

The popup watcher settles on a same-origin terminal page, but both entries in
OAUTH_POPUP_TERMINAL_PATHS were served strict same-origin COOP, which moves the
popup into its own browsing-context group. The opener could then neither read
its location nor trust window.closed, so the terminal-page branch could never
fire in production and a flow exiting through one of those pages left the row
waiting until the user happened to refocus the tab.

Serves /oauth-error and the /workspace root the same same-origin-allow-popups
their opener uses. The workspace root previously fell under the strict rule
while every /workspace/... route already got the permissive one.
The success toast is gated on the launched-attempt ref rather than the window
handle; nothing pinned that. Adds the regression test, and trims the comment
duplication the fix left behind.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptileai review

bugbot run

A closed handle and a COOP-disowned one are indistinguishable, so the watcher
published no verdict for either and relied on the focus verification to settle
it. That recovers the normal case — closing a popup hands focus back — but not
one where the opener was never blurred, leaving the row waiting indefinitely.

Arms the same safety timeout the MCP OAuth popup uses for the same reason: past
it, the row decides from the credential list rather than waiting on a verdict
that is never going to arrive. Cleared as soon as a real verdict lands.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptileai review

bugbot run

The unobservable deadline lived in the watcher effect's closure, so it was
armed only by the mount that launched the popup. The transcript virtualizes:
a row scrolled away mid-connect came back with no window handle and no blur
behind it, and nothing re-armed the bound.

Derives the deadline from the attempt's own requestedAt and arms it for any
pending attempt, so a remount inherits the time remaining rather than
restarting the clock or losing it. A demonstrably live popup still owns the
flow and is left to the watcher.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptileai review

bugbot run

Two races the previous rounds left behind.

A settle read the attempt only after its refetch, so a retry landing during
that window was resolved by a run it never triggered — failing a replacement
whose popup was still going. The attempt id is now captured before the await
and the verdict only lands if it still matches; the status is still re-read
after, so a verdict published mid-refetch is not overwritten.

The safety deadline was one-shot. A consent screen that outlived it consumed
the timeout while still live, leaving nothing to catch the popup dying
unobservably afterwards. It now re-checks at the poll interval instead of
expiring against a live window.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

Trims the COOP rationale in next.config.ts to the point, and condenses the
longest blocks in the connect hook without dropping the reasoning a reader
needs to keep the invariants.
@waleedlatif1

Copy link
Copy Markdown
Collaborator

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator

@cursor review

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f41e66c. Configure here.

@waleedlatif1
waleedlatif1 merged commit 8c6166e into staging Aug 8, 2026
30 checks passed
@waleedlatif1
waleedlatif1 deleted the fix/mship-return-to-same-tab branch August 8, 2026 06:43
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.

3 participants