Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -19547,6 +19547,40 @@
}
}
},
"command.openFolderInVSCodeInline.subtitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "VS Code Inline"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "VS Code インライン"
}
}
}
},
"command.openFolderInVSCodeInline.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Folder in VS Code (Inline)…"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フォルダを VS Code で開く(インライン)…"
}
}
}
},
"command.openSettings.subtitle": {
"extractionState": "manual",
"localizations": {
Expand Down Expand Up @@ -38571,6 +38605,57 @@
}
}
},
"menu.file.openFolderInVSCodeInline": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Folder in VS Code (Inline)…"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フォルダを VS Code で開く(インライン)…"
}
}
}
},
"menu.file.openFolderInVSCodeInline.panelPrompt": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open in VS Code"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "VS Code で開く"
}
}
}
},
"menu.file.openFolderInVSCodeInline.panelTitle": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Open Folder in VS Code (Inline)"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "フォルダを VS Code で開く(インライン)"
}
}
}
},
"menu.file.reopenClosedBrowserPanel": {
"extractionState": "manual",
"localizations": {
Expand Down
80 changes: 80 additions & 0 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5661,6 +5661,86 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
}
}

@discardableResult
func openDirectoryInInlineVSCode(
_ directoryURL: URL,
tabManager preferredTabManager: TabManager? = nil
) -> Bool {
guard let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL() else {
return false
}

let targetTabManager = preferredTabManager
?? preferredMainWindowContextForWorkspaceCreation(debugSource: "inlineVSCode.open.target")?.tabManager
guard let targetTabManager else {
return false
}

let targetWorkspaceId = targetTabManager.selectedWorkspace?.id
?? targetTabManager.tabs.first?.id
?? targetTabManager.addWorkspace(select: true).id
Comment on lines +5679 to +5681

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 Side-effectful fallback creates a workspace silently

The third branch of the ?? chain eagerly calls addWorkspace(select: true) as a side-effect of what appears to be a simple ID lookup. If selectedWorkspace is nil AND tabs.first is nil (e.g., the user has closed all tabs in the window between the panel's appear and the OK click), a brand-new, empty workspace is created just to serve as the target for the VS Code browser — which can be confusing and unexpected.

Consider guarding against this case instead:

guard let targetWorkspaceId = targetTabManager.selectedWorkspace?.id
    ?? targetTabManager.tabs.first?.id else {
    return false
}

If there are genuinely no workspaces, returning false (and letting showOpenFolderInInlineVSCodePanel emit a beep) is safer than silently creating one.

let normalizedDirectoryURL = directoryURL.standardizedFileURL

VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in
guard let serveWebURL,
let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL(
baseWebUIURL: serveWebURL,
directoryPath: normalizedDirectoryURL.path
) else {
NSSound.beep()
return
}

guard targetTabManager.openBrowser(
inWorkspace: targetWorkspaceId,
url: openFolderURL,
preferSplitRight: true
) != nil else {
Comment on lines +5679 to +5698

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Recompute workspace ID inside the async callback to avoid stale-target failures.

targetWorkspaceId is captured before ensureServeWebURL completes (Line 5679). If tabs/workspaces change while serve-web starts, openBrowser(inWorkspace:...) can fail unnecessarily.

Suggested fix
-        let targetWorkspaceId = targetTabManager.selectedWorkspace?.id
-            ?? targetTabManager.tabs.first?.id
-            ?? targetTabManager.addWorkspace(select: true).id
         let normalizedDirectoryURL = directoryURL.standardizedFileURL

         VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in
             guard let serveWebURL,
                   let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL(
                       baseWebUIURL: serveWebURL,
                       directoryPath: normalizedDirectoryURL.path
                   ) else {
                 NSSound.beep()
                 return
             }

+            let targetWorkspaceId = targetTabManager.selectedWorkspace?.id
+                ?? targetTabManager.tabs.first?.id
+                ?? targetTabManager.addWorkspace(select: true).id
             guard targetTabManager.openBrowser(
                 inWorkspace: targetWorkspaceId,
                 url: openFolderURL,
                 preferSplitRight: true
             ) != nil else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/AppDelegate.swift` around lines 5679 - 5698, targetWorkspaceId is
captured before the async ensureServeWebURL completes, causing stale workspace
selection; move the workspace-id computation into the ensureServeWebURL
completion closure (re-evaluate targetTabManager.selectedWorkspace?.id ??
targetTabManager.tabs.first?.id ?? targetTabManager.addWorkspace(select:
true).id inside the closure) and then call
targetTabManager.openBrowser(inWorkspace:url:preferSplitRight:) with that
freshly computed id so tab/workspace changes during serve startup don't cause
unnecessary failures.

NSSound.beep()
return
}
}

return true
Comment on lines +5664 to +5704

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 Behavioral change for existing context-menu action

The refactored openDirectoryInInlineVSCode delegates to openBrowser(inWorkspace:url:preferSplitRight:true), which uses topRightBrowserReusePane() to reuse an existing right-pane when the workspace is already split. The previous implementation called newBrowserSplit(from: focusedPanelId, …) unconditionally, always creating a new split directly from the right-clicked terminal.

As a result, when a workspace already has multiple panes, the right-click context-menu "Open in VS Code (Inline)" action will now reuse the top-right pane (loading a new surface there) rather than always appending a new split adjacent to the focused terminal. The test plan checks whether the context action still works, but doesn't explicitly verify the split position. It's worth confirming the new reuse-pane behaviour is intentional for the context-menu case (it may well be — fewer unwanted splits is arguably better UX).

}

func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) {
guard TerminalDirectoryOpenTarget.vscodeInline.isAvailable() else {
NSSound.beep()
return
}

let targetTabManager = preferredTabManager
?? preferredMainWindowContextForWorkspaceCreation(debugSource: "inlineVSCode.panel.target")?.tabManager
guard let targetTabManager else {
NSSound.beep()
return
}

let panel = NSOpenPanel()
panel.canChooseFiles = false
panel.canChooseDirectories = true
panel.allowsMultipleSelection = false
panel.title = String(
localized: "menu.file.openFolderInVSCodeInline.panelTitle",
defaultValue: "Open Folder in VS Code (Inline)"
)
panel.prompt = String(
localized: "menu.file.openFolderInVSCodeInline.panelPrompt",
defaultValue: "Open in VS Code"
)
if let cwd = targetTabManager.selectedWorkspace?.currentDirectory,
!cwd.isEmpty {
panel.directoryURL = URL(fileURLWithPath: cwd)
}

if panel.runModal() == .OK,
let url = panel.url,
!openDirectoryInInlineVSCode(url, tabManager: targetTabManager) {
NSSound.beep()
}
}

@objc func openWindow(
_ pasteboard: NSPasteboard,
userData: String?,
Expand Down
54 changes: 25 additions & 29 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5383,6 +5383,25 @@ struct ContentView: View {
keywords: ["open", "folder", "repository", "project", "directory"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.openFolderInVSCodeInline",
title: constant(
String(
localized: "command.openFolderInVSCodeInline.title",
defaultValue: "Open Folder in VS Code (Inline)…"
)
),
subtitle: constant(
String(
localized: "command.openFolderInVSCodeInline.subtitle",
defaultValue: "VS Code Inline"
)
),
keywords: ["open", "folder", "directory", "project", "vs", "code", "inline", "editor", "browser"],
when: { _ in TerminalDirectoryOpenTarget.vscodeInline.isAvailable() }
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newTerminalTab",
Expand Down Expand Up @@ -6077,6 +6096,11 @@ struct ContentView: View {
}
}
}
registry.register(commandId: "palette.openFolderInVSCodeInline") {
DispatchQueue.main.async {
AppDelegate.shared?.showOpenFolderInInlineVSCodePanel(tabManager: tabManager)
}
}
registry.register(commandId: "palette.newWindow") {
AppDelegate.shared?.openNewMainWindow(nil)
}
Expand Down Expand Up @@ -7475,35 +7499,7 @@ struct ContentView: View {
}

private func openFocusedDirectoryInInlineVSCode(_ directoryURL: URL) -> Bool {
guard let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL(),
let workspace = tabManager.selectedWorkspace,
let sourcePanelId = workspace.focusedPanelId else {
return false
}
let sourceTabId = workspace.id
let tabManager = tabManager
VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in
guard let serveWebURL,
let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL(
baseWebUIURL: serveWebURL,
directoryPath: directoryURL.path
) else {
NSSound.beep()
return
}
guard tabManager.newBrowserSplit(
tabId: sourceTabId,
fromPanelId: sourcePanelId,
orientation: SplitDirection.right.orientation,
insertFirst: SplitDirection.right.insertFirst,
url: openFolderURL,
focus: true
) != nil else {
NSSound.beep()
return
}
}
return true
AppDelegate.shared?.openDirectoryInInlineVSCode(directoryURL, tabManager: tabManager) ?? false
}

private func stopInlineVSCodeServeWeb() {
Expand Down
10 changes: 10 additions & 0 deletions Sources/cmuxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,16 @@ struct cmuxApp: App {
splitCommandButton(title: String(localized: "menu.file.openFolder", defaultValue: "Open Folder…"), shortcut: openFolderMenuShortcut) {
AppDelegate.shared?.showOpenFolderPanel()
}

Button(
String(
localized: "menu.file.openFolderInVSCodeInline",
defaultValue: "Open Folder in VS Code (Inline)…"
)
) {
AppDelegate.shared?.showOpenFolderInInlineVSCodePanel()
}
.disabled(!TerminalDirectoryOpenTarget.vscodeInline.isAvailable())
}

// Close tab/workspace
Expand Down
Loading