From a6971ed9208d6ddd7c9137b5b3ada2972b56df83 Mon Sep 17 00:00:00 2001 From: B Lacroix Date: Wed, 17 Jun 2026 17:01:42 +0200 Subject: [PATCH 1/2] Open each worktree in its own Zed window NSWorkspace.open hands the worktree directory to Zed as an "open document" event, so every worktree collapses into Zed's most-recent window. Launch the bundled Zed CLI (Zed.app/Contents/MacOS/cli) with the bare worktree path instead: with Zed's `cli_default_open_behavior` set to `new_window`, that opens one window per worktree and focuses the existing window when the worktree is already open. Falls back to NSWorkspace.open when the CLI helper is absent. The `-n` flag is deliberately not used: it forces a brand-new window even for an already-open worktree, producing duplicates. --- .../Clients/Workspace/WorkspaceClient.swift | 108 +++++++++++++----- supacodeTests/ZedLaunchTests.swift | 25 ++++ 2 files changed, 107 insertions(+), 26 deletions(-) create mode 100644 supacodeTests/ZedLaunchTests.swift diff --git a/supacode/Clients/Workspace/WorkspaceClient.swift b/supacode/Clients/Workspace/WorkspaceClient.swift index 317c1e310..91c95c180 100644 --- a/supacode/Clients/Workspace/WorkspaceClient.swift +++ b/supacode/Clients/Workspace/WorkspaceClient.swift @@ -31,7 +31,6 @@ private func performOpenWorktreeAction( worktree: Worktree, onError: @escaping @MainActor @Sendable (OpenActionError) -> Void ) { - let actionTitle = action.title switch action { case .editor: return @@ -39,12 +38,7 @@ private func performOpenWorktreeAction( NSWorkspace.shared.activateFileViewerSelecting([worktree.workingDirectory]) case .androidStudio, .intellij, .webstorm, .pycharm, .rubymine, .rustrover: guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else { - onError( - OpenActionError( - title: "\(action.title) not found", - message: "Install \(action.title) to open this worktree." - ) - ) + onError(.appNotFound(action)) return } let configuration = NSWorkspace.OpenConfiguration() @@ -55,24 +49,16 @@ private func performOpenWorktreeAction( return } Task { @MainActor in - onError( - OpenActionError( - title: "Unable to open in \(actionTitle)", - message: error.localizedDescription - ) - ) + onError(.openFailed(action, error)) } } + case .zed: + ZedLaunch.open(action: action, worktree: worktree, onError: onError) case .alacritty, .antigravity, .cursor, .fork, .githubDesktop, .gitkraken, .gitup, .ghostty, .kitty, .smartgit, .sourcetree, .sublimeMerge, .terminal, .vscode, .vscodeInsiders, - .vscodium, .warp, .wezterm, .windsurf, .xcode, .zed: + .vscodium, .warp, .wezterm, .windsurf, .xcode: guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else { - onError( - OpenActionError( - title: "\(action.title) not found", - message: "Install \(action.title) to open this worktree." - ) - ) + onError(.appNotFound(action)) return } NSWorkspace.shared.open( @@ -84,13 +70,83 @@ private func performOpenWorktreeAction( return } Task { @MainActor in - onError( - OpenActionError( - title: "Unable to open in \(actionTitle)", - message: error.localizedDescription - ) - ) + onError(.openFailed(action, error)) } } } } + +enum ZedLaunch { + enum Plan: Equatable { + case cli(executable: URL, arguments: [String]) + case workspaceOpen(url: URL) + } + + static func cliURL(appURL: URL) -> URL { + appURL.appending(path: "Contents/MacOS/cli") + } + + /// Open the worktree through the bundled Zed CLI with the bare path (no `-n`), falling back + /// to `NSWorkspace.open` only when the CLI helper is missing. With Zed's + /// `cli_default_open_behavior` set to `new_window`, the bare path gives one window per + /// worktree: a new window when the worktree isn't open yet, focus of the existing window when + /// it is. `-n` is avoided because it forces a new window even for an already-open worktree, + /// producing duplicates; `NSWorkspace.open` instead stacks every worktree into one window. + static func plan(cliURL: URL, cliExists: Bool, workingDirectory: URL) -> Plan { + cliExists + ? .cli(executable: cliURL, arguments: [workingDirectory.path]) + : .workspaceOpen(url: workingDirectory) + } + + @MainActor static func open( + action: OpenWorktreeAction, + worktree: Worktree, + onError: @escaping @MainActor @Sendable (OpenActionError) -> Void + ) { + guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else { + onError(.appNotFound(action)) + return + } + let cliURL = cliURL(appURL: appURL) + switch plan( + cliURL: cliURL, + cliExists: FileManager.default.fileExists(atPath: cliURL.path), + workingDirectory: worktree.workingDirectory + ) { + case .cli(let executable, let arguments): + let process = Process() + process.executableURL = executable + process.arguments = arguments + do { + try process.run() + } catch { + onError(.openFailed(action, error)) + } + case .workspaceOpen(let url): + NSWorkspace.shared.open([url], withApplicationAt: appURL, configuration: .init()) { _, error in + guard let error else { + return + } + Task { @MainActor in + onError(.openFailed(action, error)) + } + } + } + } +} + +extension OpenActionError { + static func appNotFound(_ action: OpenWorktreeAction) -> OpenActionError { + OpenActionError( + title: "\(action.title) not found", + message: "Install \(action.title) to open this worktree." + ) + } + + static func openFailed(_ action: OpenWorktreeAction, _ error: Error) -> OpenActionError { + OpenActionError( + title: "Unable to open in \(action.title)", + message: error.localizedDescription + ) + } +} diff --git a/supacodeTests/ZedLaunchTests.swift b/supacodeTests/ZedLaunchTests.swift new file mode 100644 index 000000000..2c037a36b --- /dev/null +++ b/supacodeTests/ZedLaunchTests.swift @@ -0,0 +1,25 @@ +import Foundation +import Testing + +@testable import supacode + +struct ZedLaunchTests { + private let appURL = URL(filePath: "/Applications/Zed.app") + private let workingDirectory = URL(filePath: "/Users/me/code/worktree") + + @Test func cliURLPointsInsideAppBundle() { + #expect(ZedLaunch.cliURL(appURL: appURL).path == "/Applications/Zed.app/Contents/MacOS/cli") + } + + @Test func planUsesCLIWithWorktreePathWhenCLIExists() { + let cliURL = ZedLaunch.cliURL(appURL: appURL) + let plan = ZedLaunch.plan(cliURL: cliURL, cliExists: true, workingDirectory: workingDirectory) + #expect(plan == .cli(executable: cliURL, arguments: ["/Users/me/code/worktree"])) + } + + @Test func planFallsBackToWorkspaceOpenWhenCLIMissing() { + let cliURL = ZedLaunch.cliURL(appURL: appURL) + let plan = ZedLaunch.plan(cliURL: cliURL, cliExists: false, workingDirectory: workingDirectory) + #expect(plan == .workspaceOpen(url: workingDirectory)) + } +} From ce065149f336a7252e805ef49d465acafda2f18d Mon Sep 17 00:00:00 2001 From: B Lacroix Date: Fri, 19 Jun 2026 15:39:50 +0200 Subject: [PATCH 2/2] Generalize worktree CLI launch beyond Zed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the Zed-specific launcher with a generic seam: an optional CLILauncher descriptor on OpenWorktreeAction (relative path to the app's bundled CLI helper plus any flags) and an AppLauncher that prefers a CLI launch when an app advertises one, otherwise falls back to NSWorkspace.open. Adding another CLI-capable app is now just returning a CLILauncher from `cliLauncher`. Only Zed is wired up for now; enabling other apps (VS Code, Cursor, …) needs per-app verification of their new-window semantics. --- .../Clients/Workspace/WorkspaceClient.swift | 89 +++++++++++-------- supacodeTests/AppLauncherTests.swift | 57 ++++++++++++ supacodeTests/ZedLaunchTests.swift | 25 ------ 3 files changed, 109 insertions(+), 62 deletions(-) create mode 100644 supacodeTests/AppLauncherTests.swift delete mode 100644 supacodeTests/ZedLaunchTests.swift diff --git a/supacode/Clients/Workspace/WorkspaceClient.swift b/supacode/Clients/Workspace/WorkspaceClient.swift index 91c95c180..a7ec15e3d 100644 --- a/supacode/Clients/Workspace/WorkspaceClient.swift +++ b/supacode/Clients/Workspace/WorkspaceClient.swift @@ -52,50 +52,39 @@ private func performOpenWorktreeAction( onError(.openFailed(action, error)) } } - case .zed: - ZedLaunch.open(action: action, worktree: worktree, onError: onError) case .alacritty, .antigravity, .cursor, .fork, .githubDesktop, .gitkraken, .gitup, .ghostty, .kitty, .smartgit, .sourcetree, .sublimeMerge, .terminal, .vscode, .vscodeInsiders, - .vscodium, .warp, .wezterm, .windsurf, .xcode: - guard let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: action.bundleIdentifier) else { - onError(.appNotFound(action)) - return - } - NSWorkspace.shared.open( - [worktree.workingDirectory], - withApplicationAt: appURL, - configuration: .init() - ) { _, error in - guard let error else { - return - } - Task { @MainActor in - onError(.openFailed(action, error)) - } - } + .vscodium, .warp, .wezterm, .windsurf, .xcode, .zed: + AppLauncher.open(action: action, worktree: worktree, onError: onError) } } -enum ZedLaunch { +/// Opens a worktree directory in an arbitrary macOS app, preferring the app's bundled +/// command-line helper when it advertises one via `OpenWorktreeAction.cliLauncher`. +/// +/// A CLI launch is preferred because it lets us pass flags/arguments the plain +/// "open document" event (`NSWorkspace.open`) cannot — e.g. Zed honoring +/// `cli_default_open_behavior` to give one window per worktree. Apps without a launcher, +/// and any app whose helper is missing, fall back to `NSWorkspace.open`. +enum AppLauncher { enum Plan: Equatable { case cli(executable: URL, arguments: [String]) case workspaceOpen(url: URL) } - static func cliURL(appURL: URL) -> URL { - appURL.appending(path: "Contents/MacOS/cli") - } - - /// Open the worktree through the bundled Zed CLI with the bare path (no `-n`), falling back - /// to `NSWorkspace.open` only when the CLI helper is missing. With Zed's - /// `cli_default_open_behavior` set to `new_window`, the bare path gives one window per - /// worktree: a new window when the worktree isn't open yet, focus of the existing window when - /// it is. `-n` is avoided because it forces a new window even for an already-open worktree, - /// producing duplicates; `NSWorkspace.open` instead stacks every worktree into one window. - static func plan(cliURL: URL, cliExists: Bool, workingDirectory: URL) -> Plan { - cliExists - ? .cli(executable: cliURL, arguments: [workingDirectory.path]) - : .workspaceOpen(url: workingDirectory) + static func plan( + launcher: OpenWorktreeAction.CLILauncher?, + appURL: URL, + cliExists: Bool, + workingDirectory: URL + ) -> Plan { + guard let launcher, cliExists else { + return .workspaceOpen(url: workingDirectory) + } + return .cli( + executable: appURL.appending(path: launcher.relativeExecutablePath), + arguments: launcher.openArguments + [workingDirectory.path] + ) } @MainActor static func open( @@ -107,10 +96,14 @@ enum ZedLaunch { onError(.appNotFound(action)) return } - let cliURL = cliURL(appURL: appURL) + let launcher = action.cliLauncher + let cliExists = + launcher.map { FileManager.default.fileExists(atPath: appURL.appending(path: $0.relativeExecutablePath).path) } + ?? false switch plan( - cliURL: cliURL, - cliExists: FileManager.default.fileExists(atPath: cliURL.path), + launcher: launcher, + appURL: appURL, + cliExists: cliExists, workingDirectory: worktree.workingDirectory ) { case .cli(let executable, let arguments): @@ -135,6 +128,28 @@ enum ZedLaunch { } } +extension OpenWorktreeAction { + /// Describes how to open a worktree through an app's bundled command-line helper. + struct CLILauncher: Equatable { + /// Path to the helper executable, relative to the app bundle root. + let relativeExecutablePath: String + /// Flags placed before the worktree path; the path is always appended last. + let openArguments: [String] + } + + /// The bundled CLI used to open a worktree, or `nil` to fall back to `NSWorkspace.open`. + var cliLauncher: CLILauncher? { + switch self { + case .zed: + // The bare path (no `-n`) gives one window per worktree when Zed's + // `cli_default_open_behavior` is `new_window`, focusing an already-open worktree. + CLILauncher(relativeExecutablePath: "Contents/MacOS/cli", openArguments: []) + default: + nil + } + } +} + extension OpenActionError { static func appNotFound(_ action: OpenWorktreeAction) -> OpenActionError { OpenActionError( diff --git a/supacodeTests/AppLauncherTests.swift b/supacodeTests/AppLauncherTests.swift new file mode 100644 index 000000000..34278a55e --- /dev/null +++ b/supacodeTests/AppLauncherTests.swift @@ -0,0 +1,57 @@ +import Foundation +import SupacodeSettingsShared +import Testing + +@testable import supacode + +struct AppLauncherTests { + private let appURL = URL(filePath: "/Applications/Zed.app") + private let workingDirectory = URL(filePath: "/Users/me/code/worktree") + + @Test func zedExposesBundledCLILauncher() { + #expect( + OpenWorktreeAction.zed.cliLauncher + == .init(relativeExecutablePath: "Contents/MacOS/cli", openArguments: []) + ) + } + + @Test func appsWithoutABundledCLIHaveNoLauncher() { + #expect(OpenWorktreeAction.cursor.cliLauncher == nil) + #expect(OpenWorktreeAction.finder.cliLauncher == nil) + } + + @Test func planLaunchesCLIWithWorktreePathWhenAvailable() { + let plan = AppLauncher.plan( + launcher: OpenWorktreeAction.zed.cliLauncher, + appURL: appURL, + cliExists: true, + workingDirectory: workingDirectory + ) + #expect( + plan == .cli( + executable: appURL.appending(path: "Contents/MacOS/cli"), + arguments: ["/Users/me/code/worktree"] + ) + ) + } + + @Test func planFallsBackToWorkspaceOpenWhenCLIMissing() { + let plan = AppLauncher.plan( + launcher: OpenWorktreeAction.zed.cliLauncher, + appURL: appURL, + cliExists: false, + workingDirectory: workingDirectory + ) + #expect(plan == .workspaceOpen(url: workingDirectory)) + } + + @Test func planFallsBackToWorkspaceOpenWhenNoLauncher() { + let plan = AppLauncher.plan( + launcher: nil, + appURL: appURL, + cliExists: true, + workingDirectory: workingDirectory + ) + #expect(plan == .workspaceOpen(url: workingDirectory)) + } +} diff --git a/supacodeTests/ZedLaunchTests.swift b/supacodeTests/ZedLaunchTests.swift deleted file mode 100644 index 2c037a36b..000000000 --- a/supacodeTests/ZedLaunchTests.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import Testing - -@testable import supacode - -struct ZedLaunchTests { - private let appURL = URL(filePath: "/Applications/Zed.app") - private let workingDirectory = URL(filePath: "/Users/me/code/worktree") - - @Test func cliURLPointsInsideAppBundle() { - #expect(ZedLaunch.cliURL(appURL: appURL).path == "/Applications/Zed.app/Contents/MacOS/cli") - } - - @Test func planUsesCLIWithWorktreePathWhenCLIExists() { - let cliURL = ZedLaunch.cliURL(appURL: appURL) - let plan = ZedLaunch.plan(cliURL: cliURL, cliExists: true, workingDirectory: workingDirectory) - #expect(plan == .cli(executable: cliURL, arguments: ["/Users/me/code/worktree"])) - } - - @Test func planFallsBackToWorkspaceOpenWhenCLIMissing() { - let cliURL = ZedLaunch.cliURL(appURL: appURL) - let plan = ZedLaunch.plan(cliURL: cliURL, cliExists: false, workingDirectory: workingDirectory) - #expect(plan == .workspaceOpen(url: workingDirectory)) - } -}