Skip to content

fix(browser): open Builderlab account connections in the system browser - #3287

Merged
nathan-thillairajah merged 4 commits into
mainfrom
dylan/hosted-app-popout-links
Nov 11, 2026
Merged

nathan-thillairajah merged 4 commits into
mainfrom
dylan/hosted-app-popout-links

Conversation

@nathan-thillairajah

@nathan-thillairajah nathan-thillairajah commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Builderlab tool apps that use GitHub, Linear and similar services send you to a Builderlab "connections" page to link your account. That page needs a regular Builderlab web login, and Buzz's in-app browser doesn't have one. So clicking Connect in a hosted app inside Buzz loaded a portal login page inside the panel, where it stalled. #2708 fixed the same problem for the one-time tool approval page by opening it in your default browser. It still left you to click Reload when you came back.

Now:

  • When a hosted app links to its own connections page, Buzz opens that page in your default browser instead. Apps use a plain same-tab link, so the link is caught the same way fix(browser): open Builderlab tool-app approval in the system browser #2708 catches the approval page. Only a page for the app you're looking at is handed off, and Buzz rebuilds its URL with only the extension and app parameters.
  • When you come back to Buzz after approving or connecting, the app reloads by itself. Reload still works.
  • As in fix(browser): open Builderlab tool-app approval in the system browser #2708, Buzz opens one browser tab by itself until you next click Reload, Back or Forward or use the address bar. Clicks inside the app don't count. A later Connect, for example Linear after GitHub or after approving the app, shows "This app wants to open a Builderlab page in your web browser" with an Open in browser button. Clicking it opens that page and counts as the one tab. Coming back to Buzz doesn't count either, so an app that asks again on load can't keep pulling you back to the browser. Loading another page or closing the tab drops the held page.

Not covered: target=_blank and window.open links. Hosted apps run under a CSP sandbox with no allow-popups (compose HostedSiteAppResponseHeaders), so WebKit blocks those before Buzz sees them. I checked this with a local WKWebView harness, with and without that header. Other new-window links are still dropped. No in-app OAuth, no kgoose changes, no new flag. macOS only, like the in-app browser itself.

Testing: unit tests cover the connections URL matching, the one-tab and held rules, and that coming back returns only once and only after a tab actually opened. They also cover the toolbar button. The focus handler and its load (rather than navigate) wiring have no test, since that needs a live window. The full src-tauri suite passes, except window_state::tests::onboarding_snapshot_is_kept_until_next_launch_restore, which segfaults locally on main too. Hand-tested on a local build of 1f83d365f: clicking Connect in a GitHub tool app opened the connections page in the default browser, and the app reloaded on its own after switching back to Buzz.

BOT-2666

🤖 Generated with Claude Code

A hosted app's Connect link goes to the Builderlab portal's connections
page, which needs the portal web login the in-app browser lacks. Hand it
to the system browser the way #2708 hands off the approval gate, reload
the app once when Buzz is focused again, and hold a later request behind
an Open in browser button under the same one-tab-per-user-action guard.

BOT-2666

Co-authored-by: Dylan G <69953f73c9f5149d56f2155107338a5435823b938a6c430c63e779f9a8a446f9@remote-agent-pilot.communities.buzz.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Nathan Thillairajah <[email protected]>
@nathan-thillairajah
nathan-thillairajah marked this pull request as ready for review November 11, 2026 17:55
@nathan-thillairajah
nathan-thillairajah requested a review from a team as a code owner November 11, 2026 17:55
snapshot.notice = Some(APPROVE_AFTER_RELOAD.into());
snapshot.loading = false;
}
// The cancelled load never finishes.

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.

🤖 [P3] Clear the held connection when an approval takes over the notice. If a connection page is already held and this guest reaches the approval gate with opened set, this branch sets the approval notice/pending page but leaves held populated. The UI therefore offers Open in browser beside the approval notice, but opens the old connection page; that handoff also replaces the pending approval. Keep the notice and deferred action owned by the same page, for example by clearing held when approval becomes pending. This is a source-traced scenario, not a native runtime reproduction.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 993b587: when an approval takes over the notice, the held connections page is cleared, so Open in browser can't open it in the approval's place. a_connection_after_an_opened_approval_waits_for_open_in_browser now reaches the gate after a held connection and asserts held is cleared and pending is set. It fails without the fix.

match next {
Some(next) => self.load(&next.app, false),
None => Ok(()),
}

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.

🤖 [P3] Cover the production return path preserving the automatic-tab limit. Replacing self.load with self.navigate in returned() still passes all 35 browser:: tests, although navigate resets opened and would allow another automatic tab on each return for an app that requests connection on every load. Add a regression test exercising this return decision and asserting the tab limit survives; the existing handoff model tests do not cover this wiring.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 993b587, structurally rather than through a seam. navigate no longer resets the approvals. The only reset is at the user-action call sites: the typed-address command, and Reload/Back/Forward as before. So returned() goes through the same navigate as everything else, and there's no resetting variant it could be switched to. I didn't add a test for the Focused(true) → returned() wiring because BrowserSession needs a live WKWebView. The model-level test (coming_back_returns_once_to_the_page_after_an_opened_approval) still asserts that the tab limit survives the return.

Drop a held connections page when an approval takes over the notice, and
reset the approvals only where a user acts (a typed address), not inside
navigate, so the return-on-focus load cannot re-arm the tab limit.

Co-authored-by: Dylan G <69953f73c9f5149d56f2155107338a5435823b938a6c430c63e779f9a8a446f9@remote-agent-pilot.communities.buzz.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Nathan Thillairajah <[email protected]>
@dr0uleau
dr0uleau requested a review from a team November 11, 2026 18:41

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

Kyle reviewed this PR and the Nexus review-tracker notes. Approved with the non-blocking route-preservation comment below.

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

Comment thread src-tauri/src/browser/mod.rs Outdated
/// The page the guest shows, when it is on `app`: only the app being
/// opened hands off its own portal pages.
fn shown(&self, app: &str) -> Option<Url> {
let current = Url::parse(&self.snapshot.borrow().url).ok()?;

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] Capture the live route before handing off

Could this capture the guest’s current URL rather than the shared snapshot? After an SPA pushState, snapshot() updates only its returned clone, while shown() still reads the earlier URL. The new focus-return path then navigates to that earlier route instead of the page where the user clicked Connect. Reading the live URL at handoff would preserve that page.

Posted by Codex (gpt-6.1-sol) on behalf of kylez.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🤖 Fixed in 22dde1e. The handoff now reads the guest's own URL when it intercepts (WKWebView URL, held weakly through a closure set once the guest exists), and falls back to the snapshot. New test coming_back_returns_to_the_route_the_app_moved_to_itself fails without this change.

nathan-thillairajah and others added 2 commits November 11, 2026 12:20
A hosted app that changes its route with pushState reports no page
load, so the shared snapshot kept the earlier URL and coming back from
the system browser reloaded that route. The handoff now reads the
guest's own URL, falling back to the snapshot.

Co-authored-by: Dylan G <69953f73c9f5149d56f2155107338a5435823b938a6c430c63e779f9a8a446f9@remote-agent-pilot.communities.buzz.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Nathan Thillairajah <[email protected]>
The CI run for 22dde1e stayed queued and could not be cancelled or
rerun.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Nathan Thillairajah <[email protected]>
@dr0uleau
dr0uleau requested a review from a team November 11, 2026 22:13
@nathan-thillairajah
nathan-thillairajah merged commit 06d99be into main Nov 11, 2026
50 checks passed
@nathan-thillairajah
nathan-thillairajah deleted the dylan/hosted-app-popout-links branch November 11, 2026 22:33
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* origin/main:
  fix(identity): show a backed-up key as backed up, and drop dead retries (block#3223)
  fix(browser): open Builderlab account connections in the system browser (block#3287)
  Expose exact captured-message reads (block#3264)
  docs(agents): keep internal discussion out of public repo content (block#3258)
  fix(identity): clear a copied private key, name restore failures, fix storage copy (block#3236)
  fix(channels): list workspaces that load after `#` opens (block#3277)
  feat(profiles): show a larger avatar in the hover profile preview (block#3232)
  fix(identity): ask for credentials on the next sign-in after signing out (block#3231)
  fix(relay): keep saved channels when the community refuses sign-in (block#3215)

Signed-off-by: Joah Gerstenberg <[email protected]>
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 12, 2026
* origin/main: (38 commits)
  Fix reminder counts with compact dock corner tabs (block#3126)
  Add conditional undo for explicit reads (block#3291)
  fix(composer): edit channel links as atomic chips (block#3312)
  fix(agents): show remote instruction limit (block#3325)
  NEXUS-4033: Offer session skills in the core task composer (block#2790)
  feat(github): name pasted repository and commit links (block#3268)
  Let plugins give standing instructions to every agent turn (block#3285)
  fix(relay): read a channel's creation once, not per metadata version (block#3244)
  fix(messages): remove unconfirmed sends from this device (block#3317)
  fix: expose proven non-delivery for artifacts (block#3300)
  feat(plugins): let plugins mark a person's name in @mention suggestions (block#3260)
  fix(mcp): strip metadata from images agents attach (block#3311)
  fix(identity): name a linked key that isn't here, and keep Restore after a failed backup check (block#3238)
  feat(changesets): show the files a local agent changed on its reply (block#3214)
  fix(identity): show a backed-up key as backed up, and drop dead retries (block#3223)
  fix(browser): open Builderlab account connections in the system browser (block#3287)
  Expose exact captured-message reads (block#3264)
  docs(agents): keep internal discussion out of public repo content (block#3258)
  fix(identity): clear a copied private key, name restore failures, fix storage copy (block#3236)
  fix(channels): list workspaces that load after `#` opens (block#3277)
  ...
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