From 57990b8d5d424613c4e2a8395caeebd7607baf78 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:39:49 +1000 Subject: [PATCH 1/6] Run ide browse's open in the foreground so cmux's shim catches it --- scripts/install-cli-command.sh | 6 +++++- scripts/validate-launch-runners.mjs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/scripts/install-cli-command.sh b/scripts/install-cli-command.sh index 2b4278d..e18263a 100755 --- a/scripts/install-cli-command.sh +++ b/scripts/install-cli-command.sh @@ -139,7 +139,11 @@ if [ "\${1:-}" = "browse" ]; then done fi - open "\$browse_url" >/dev/null 2>&1 & + # Foreground, not backgrounded: under cmux the \`open\` shim hands the URL to a + # \`cmux browser open\` child, and if this wrapper exits first the child is orphaned, + # loses its pane/session context, and the shim falls back to the system browser. + # \`open\` returns as soon as it dispatches the URL, so foregrounding costs nothing. + open "\$browse_url" >/dev/null 2>&1 exit 0 fi diff --git a/scripts/validate-launch-runners.mjs b/scripts/validate-launch-runners.mjs index a67e0b5..ad2bf4a 100644 --- a/scripts/validate-launch-runners.mjs +++ b/scripts/validate-launch-runners.mjs @@ -112,7 +112,7 @@ try { assertIncludes(ideCommand, 'browse_url="http://localhost:17877/browse?path=$(url_encode "$browse_path")"'); assertIncludes(ideCommand, 'IDE_BROWSE_PATH="$browse_path" open "$APP_BUNDLE" --args browse "$browse_path"'); assertIncludes(ideCommand, 'until running_app_reachable; do'); - assertIncludes(ideCommand, 'open "$browse_url" >/dev/null 2>&1 &'); + assertIncludes(ideCommand, 'open "$browse_url" >/dev/null 2>&1'); assertOrdered(ideCommand, 'if [ "${1:-}" = "browse" ]; then', 'ARGS=()'); assertIncludes(ideCommand, 'ARGS=()'); assertIncludes(ideCommand, 'ARGS+=("$(cd "$arg" && pwd -P)")'); From 7a029b987789537f4d783cc1cf45d6cd8067750f Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:39:49 +1000 Subject: [PATCH 2/6] Show one GitHub commit link, preferring the branch's tracking remote --- src-tauri/src/git_attribution.rs | 92 +++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/git_attribution.rs b/src-tauri/src/git_attribution.rs index f279498..8ffe74b 100644 --- a/src-tauri/src/git_attribution.rs +++ b/src-tauri/src/git_attribution.rs @@ -103,7 +103,10 @@ pub(crate) async fn attribution_for_file(workspace_root: &Path, relative: &str) Ok(commit) => commit, Err(_) => return unsupported(relative, "File has no local commit history"), }; - let remote_templates = remote_templates(&repo); + let remote_templates = pick_primary_template( + remote_templates(&repo), + tracking_remote_name(&repo).as_deref(), + ); let blame = match repo.blame_file( repo_relative_bstr.as_bstr(), @@ -488,6 +491,54 @@ fn github_remote_template(remote_name: &str, remote_url: &str) -> Option Option { + let head = repo.head().ok()?; + let ref_name = head.referent_name()?; + let tracking = repo + .branch_remote_tracking_ref_name(ref_name, gix::remote::Direction::Fetch)? + .ok()?; + // The tracking ref is `refs/remotes//`; the remote name is the first + // path segment after that prefix. + tracking + .as_bstr() + .to_str() + .ok()? + .strip_prefix("refs/remotes/")? + .split('/') + .next() + .map(str::to_string) +} + +// Reduce the GitHub remotes to the single one whose commit link is meaningful: the +// branch's upstream, then `origin`, then the first as a last resort. A single remote is +// returned untouched. +fn pick_primary_template( + mut templates: Vec, + tracking_remote: Option<&str>, +) -> Vec { + if templates.len() <= 1 { + return templates; + } + let index = tracking_remote + .and_then(|name| { + templates + .iter() + .position(|template| template.remote_name == name) + }) + .or_else(|| { + templates + .iter() + .position(|template| template.remote_name == "origin") + }) + .unwrap_or(0); + vec![templates.swap_remove(index)] +} + fn lossless_string(value: &BStr) -> String { value.to_str_lossy().trim().to_string() } @@ -590,6 +641,45 @@ mod tests { assert!(github_remote_template("origin", "git@gitlab.com:org/repo.git").is_none()); } + #[test] + fn pick_primary_template_prefers_tracking_then_origin() { + let template = |name: &str| RemoteTemplate { + provider: "GitHub".to_string(), + remote_name: name.to_string(), + base_url: format!("https://github.com/acme/{name}"), + }; + + // A single remote is returned untouched. + assert_eq!( + pick_primary_template(vec![template("origin")], None), + vec![template("origin")] + ); + + // The branch's upstream wins when present. + assert_eq!( + pick_primary_template( + vec![template("origin"), template("chat-bot")], + Some("chat-bot") + ), + vec![template("chat-bot")] + ); + + // No/unknown upstream falls back to origin. + assert_eq!( + pick_primary_template(vec![template("chat-bot"), template("origin")], None), + vec![template("origin")] + ); + + // No upstream and no origin falls back to the first remote. + assert_eq!( + pick_primary_template( + vec![template("chat-bot"), template("upstream")], + Some("nope") + ), + vec![template("chat-bot")] + ); + } + #[tokio::test] async fn returns_local_file_and_line_attribution() { let dir = tempdir().unwrap(); From c5f24f913edf52b44b1f36d8015127a8a8861503 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:39:50 +1000 Subject: [PATCH 3/6] Re-show the main window on dock reopen after a browse launch --- src-tauri/src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 215ad52..d361f24 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2100,6 +2100,23 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while building application") .run(|app, event| { + // A browse launch hides the `main` window and opens no visible window, so a + // dock-icon click reactivates an app with nothing to focus. Re-show + focus + // `main` (only hidden, never destroyed) so the dock icon can always summon it. + #[cfg(target_os = "macos")] + if let tauri::RunEvent::Reopen { + has_visible_windows, + .. + } = &event + { + if !*has_visible_windows { + if let Some(main_window) = app.get_webview_window("main") { + let _ = main_window.show(); + let _ = main_window.set_focus(); + } + } + } + #[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))] if let tauri::RunEvent::Opened { urls } = event { let requests = open_launch_requests_for_urls(urls); From f4755ad5b2adbed17739cb4c552efc029c93c50f Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:46:52 +1000 Subject: [PATCH 4/6] Reuse focus_window in the Reopen handler focus_window also restores a minimized window, unlike the raw show+set_focus pair. --- src-tauri/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d361f24..c288cc1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2111,8 +2111,7 @@ pub fn run() { { if !*has_visible_windows { if let Some(main_window) = app.get_webview_window("main") { - let _ = main_window.show(); - let _ = main_window.set_focus(); + focus_window(&main_window); } } } From 22d69eff0a9d6cd8e6985697931627c3ee6e486f Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:53:51 +1000 Subject: [PATCH 5/6] Derive the tracking remote from branch config, not the tracking ref path A remote name containing a slash (e.g. team/fork) made refs/remotes// ambiguous to split on '/'. Read branch..remote via gix's branch_remote_name instead, and add a regression test for a slashed remote name. --- src-tauri/src/git_attribution.rs | 43 ++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/git_attribution.rs b/src-tauri/src/git_attribution.rs index 8ffe74b..f53737c 100644 --- a/src-tauri/src/git_attribution.rs +++ b/src-tauri/src/git_attribution.rs @@ -499,18 +499,15 @@ fn github_remote_template(remote_name: &str, remote_url: &str) -> Option Option { let head = repo.head().ok()?; let ref_name = head.referent_name()?; - let tracking = repo - .branch_remote_tracking_ref_name(ref_name, gix::remote::Direction::Fetch)? - .ok()?; - // The tracking ref is `refs/remotes//`; the remote name is the first - // path segment after that prefix. - tracking + // Read `branch..remote` directly rather than parsing the tracking ref path — + // a remote name containing a slash (e.g. `team/fork`) makes `refs/remotes//` + // ambiguous to split. gix itself treats any slashed name as a `Name::Url` (it can't tell a + // slashed remote name from a URL either), so read the raw string rather than branching on + // the Symbol/Url distinction — the caller matches it against known remote names either way. + repo.branch_remote_name(ref_name.shorten(), gix::remote::Direction::Fetch)? .as_bstr() .to_str() - .ok()? - .strip_prefix("refs/remotes/")? - .split('/') - .next() + .ok() .map(str::to_string) } @@ -680,6 +677,32 @@ mod tests { ); } + #[test] + fn tracking_remote_name_handles_a_slash_in_the_remote_name() { + let dir = tempdir().unwrap(); + init_repo(dir.path()); + fs::write(dir.path().join("README.md"), "first\n").unwrap(); + commit_all(dir.path(), "Add readme", "1700000000 +0000"); + run_git( + dir.path(), + ["remote", "add", "team/fork", "git@github.com:acme/fork.git"], + ); + run_git(dir.path(), ["config", "branch.main.remote", "team/fork"]); + run_git( + dir.path(), + ["config", "branch.main.merge", "refs/heads/main"], + ); + + let repo = gix::discover(dir.path()).unwrap(); + + assert_eq!( + tracking_remote_name(&repo).as_deref(), + Some("team/fork"), + "a remote name containing a slash must resolve via branch..remote, \ + not by splitting the refs/remotes// tracking path" + ); + } + #[tokio::test] async fn returns_local_file_and_line_attribution() { let dir = tempdir().unwrap(); From cfe6ce163730ca81f0755a88606e6e56b64402f4 Mon Sep 17 00:00:00 2001 From: Gordon Beeming Date: Mon, 13 Jul 2026 16:53:55 +1000 Subject: [PATCH 6/6] Reject a regression to the backgrounded browse open in validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior assertion only checked a substring, so it still passed if the launcher regressed to 'open "$browse_url" >/dev/null 2>&1 &' — exactly the cmux-orphaning form this PR fixes. --- scripts/validate-launch-runners.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/validate-launch-runners.mjs b/scripts/validate-launch-runners.mjs index ad2bf4a..c36cfdc 100644 --- a/scripts/validate-launch-runners.mjs +++ b/scripts/validate-launch-runners.mjs @@ -113,6 +113,7 @@ try { assertIncludes(ideCommand, 'IDE_BROWSE_PATH="$browse_path" open "$APP_BUNDLE" --args browse "$browse_path"'); assertIncludes(ideCommand, 'until running_app_reachable; do'); assertIncludes(ideCommand, 'open "$browse_url" >/dev/null 2>&1'); + assertNotIncludes(ideCommand, 'open "$browse_url" >/dev/null 2>&1 &'); assertOrdered(ideCommand, 'if [ "${1:-}" = "browse" ]; then', 'ARGS=()'); assertIncludes(ideCommand, 'ARGS=()'); assertIncludes(ideCommand, 'ARGS+=("$(cd "$arg" && pwd -P)")');