From 61cb5f7943a7305069826efd06442b7adcd3cf57 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:09:33 -0700 Subject: [PATCH 01/18] Add code editor surface type --- Resources/Localizable.xcstrings | 14 ++ Sources/AppDelegate.swift | 78 +++++++- Sources/CmuxConfig.swift | 35 ++++ Sources/CmuxLifecycleEventPublishing.swift | 2 + Sources/CmuxSurfaceTabBarBuiltInAction.swift | 7 +- Sources/ContentView.swift | 45 +++++ Sources/KeyboardShortcutContext.swift | 2 + .../BrowserOmnibarPerformanceSupport.swift | 7 +- Sources/Panels/BrowserPanel.swift | 91 ++++++++- Sources/Panels/BrowserPanelView.swift | 12 +- Sources/Panels/Panel.swift | 35 +++- Sources/Panels/PanelContentView.swift | 4 +- Sources/Search/GlobalSearchDocuments.swift | 2 +- .../GlobalSearchPanelCaptureManager.swift | 9 +- Sources/TabManager.swift | 164 +++++++++++++--- Sources/TaskManagerSnapshot.swift | 16 +- Sources/TaskManagerTypes.swift | 3 + Sources/TerminalController.swift | 57 +++++- ...minalControllerV2ParamParsingSupport.swift | 5 +- Sources/TerminalPaneDropTargetView.swift | 2 +- Sources/Workspace.swift | 179 ++++++++++++++++-- Sources/WorkspaceContentView.swift | 29 ++- Sources/cmuxApp.swift | 5 + .../TabManagerSessionSnapshotTests.swift | 30 +++ cmuxTests/TabManagerUnitTests.swift | 30 +++ 25 files changed, 782 insertions(+), 81 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 12e580643703..f36ee3805d54 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -2,6 +2,20 @@ "sourceLanguage": "en", "version": "1.0", "strings": { + "codeEditor.newTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, + "command.codeEditorSplitDown.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor Layout" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタレイアウト" } } } }, + "command.codeEditorSplitDown.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Split Code Editor Down" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタを下に分割" } } } }, + "command.codeEditorSplitRight.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor Layout" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタレイアウト" } } } }, + "command.codeEditorSplitRight.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Split Code Editor Right" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタを右に分割" } } } }, + "command.newCodeEditorTab.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Tab" } }, "ja": { "stringUnit": { "state": "translated", "value": "タブ" } } } }, + "command.newCodeEditorTab.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "New Tab (Code Editor)" } }, "ja": { "stringUnit": { "state": "translated", "value": "新規タブ(コードエディタ)" } } } }, + "commandPalette.kind.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, + "emptyPane.browser": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Browser" } }, "ja": { "stringUnit": { "state": "translated", "value": "ブラウザ" } } } }, + "emptyPane.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, + "emptyPane.terminal": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } }, + "emptyPane.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Empty Panel" } }, "ja": { "stringUnit": { "state": "translated", "value": "空のパネル" } } } }, + "menu.file.newCodeEditorTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "New Code Editor Tab" } }, "ja": { "stringUnit": { "state": "translated", "value": "新しいコードエディタタブ" } } } }, + "taskManager.row.surfaceType.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, "cli.hooks.error.configDirectoryIsFile": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "cmux could not create the hooks directory: a file exists at %@; remove or rename the conflicting file and re-run `cmux hooks setup`" } }, "ja": { "stringUnit": { "state": "translated", "value": "cmux は hooks ディレクトリを作成できませんでした: %@ にファイルがあります。競合するファイルを削除または名前変更してから `cmux hooks setup` を再実行してください" } } } }, "cli.hooks.antigravity.aborted": { "extractionState": "manual", "localizations": {"ar":{"stringUnit":{"state":"translated","value":"Aborted."}},"bs":{"stringUnit":{"state":"translated","value":"Aborted."}},"da":{"stringUnit":{"state":"translated","value":"Aborted."}},"de":{"stringUnit":{"state":"translated","value":"Aborted."}},"en":{"stringUnit":{"state":"translated","value":"Aborted."}},"es":{"stringUnit":{"state":"translated","value":"Aborted."}},"fr":{"stringUnit":{"state":"translated","value":"Aborted."}},"it":{"stringUnit":{"state":"translated","value":"Aborted."}},"ja":{"stringUnit":{"state":"translated","value":"中止しました。"}},"ko":{"stringUnit":{"state":"translated","value":"Aborted."}},"nb":{"stringUnit":{"state":"translated","value":"Aborted."}},"pl":{"stringUnit":{"state":"translated","value":"Aborted."}},"pt-BR":{"stringUnit":{"state":"translated","value":"Aborted."}},"ru":{"stringUnit":{"state":"translated","value":"Aborted."}},"th":{"stringUnit":{"state":"translated","value":"Aborted."}},"tr":{"stringUnit":{"state":"translated","value":"Aborted."}},"uk":{"stringUnit":{"state":"translated","value":"Aborted."}},"zh-Hans":{"stringUnit":{"state":"translated","value":"Aborted."}},"zh-Hant":{"stringUnit":{"state":"translated","value":"Aborted."}}} }, "cli.hooks.antigravity.alreadyUpToDate": { "extractionState": "manual", "localizations": {"ar":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"bs":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"da":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"de":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"en":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"es":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"fr":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"it":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"ja":{"stringUnit":{"state":"translated","value":"%@ hooks はすでに %@ で最新です"}},"ko":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"nb":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"pl":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"pt-BR":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"ru":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"th":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"tr":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"uk":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}}} }, diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index fefd060ca08f..4e314b757513 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6814,7 +6814,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } - guard targetTabManager.openBrowser( + guard targetTabManager.openCodeEditor( inWorkspace: targetWorkspaceId, url: openFolderURL, preferSplitRight: true @@ -6827,6 +6827,76 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return true } + @discardableResult + func openCodeEditor( + tabManager preferredTabManager: TabManager? = nil, + url requestedURL: URL? = nil, + preferSplitRight: Bool = false, + insertAtEnd: Bool = false + ) -> UUID? { + let targetTabManager = preferredTabManager + ?? preferredMainWindowContextForWorkspaceCreation(debugSource: "codeEditor.open.target")?.tabManager + guard let targetTabManager else { + NSSound.beep() + return nil + } + + let targetWorkspaceId = targetTabManager.selectedWorkspace?.id + ?? targetTabManager.tabs.first?.id + ?? targetTabManager.addWorkspace(select: true).id + + if let requestedURL { + return targetTabManager.openCodeEditor( + inWorkspace: targetWorkspaceId, + url: requestedURL, + preferSplitRight: preferSplitRight, + insertAtEnd: insertAtEnd + ) + } + + func openDefaultCodeEditor() -> UUID? { + targetTabManager.openCodeEditor( + inWorkspace: targetWorkspaceId, + url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + preferSplitRight: preferSplitRight, + insertAtEnd: insertAtEnd + ) + } + + let directoryPath = targetTabManager.selectedWorkspace?.currentDirectory + .trimmingCharacters(in: .whitespacesAndNewlines) + guard let directoryPath, + !directoryPath.isEmpty, + let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL() else { + return openDefaultCodeEditor() + } + + VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + guard let serveWebURL, + let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( + baseWebUIURL: serveWebURL, + directoryPath: directoryPath + ) else { + if openDefaultCodeEditor() == nil { + NSSound.beep() + } + return + } + + guard targetTabManager.openCodeEditor( + inWorkspace: targetWorkspaceId, + url: openFolderURL, + preferSplitRight: preferSplitRight, + insertAtEnd: insertAtEnd + ) != nil else { + NSSound.beep() + return + } + } + + return nil + } + func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) { guard TerminalDirectoryOpenTarget.vscodeInline.isAvailable() else { NSSound.beep() @@ -13825,6 +13895,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent } onExecuted?() return true + case .newCodeEditor: + guard context.tabManager.openCodeEditor(insertAtEnd: true) != nil else { + return false + } + onExecuted?() + return true case .splitRight: if shouldSuppressSplitShortcutForTransientTerminalFocusState( direction: .right, diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift index 4d47710ae764..ca48beb8f930 100644 --- a/Sources/CmuxConfig.swift +++ b/Sources/CmuxConfig.swift @@ -1124,12 +1124,14 @@ struct CmuxSurfaceTabBarButton: Codable, Sendable, Hashable, Identifiable { static let newTerminal = actionReference(CmuxSurfaceTabBarBuiltInAction.newTerminal.configID) static let newBrowser = actionReference(CmuxSurfaceTabBarBuiltInAction.newBrowser.configID) + static let newCodeEditor = actionReference(CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID) static let splitRight = actionReference(CmuxSurfaceTabBarBuiltInAction.splitRight.configID) static let splitDown = actionReference(CmuxSurfaceTabBarBuiltInAction.splitDown.configID) static let defaults: [CmuxSurfaceTabBarButton] = [ .newTerminal, .newBrowser, + .newCodeEditor, .splitRight, .splitDown ] @@ -1514,6 +1516,9 @@ struct CmuxResolvedConfigAction: Identifiable, Sendable, Hashable { case .newBrowser: title = String(localized: "command.newBrowserTab.title", defaultValue: "New Browser Tab") keywords = ["new", "browser", "tab", "surface"] + case .newCodeEditor: + title = String(localized: "command.newCodeEditorTab.title", defaultValue: "New Code Editor Tab") + keywords = ["new", "code", "editor", "tab", "surface", "vscode"] case .splitRight: title = String(localized: "command.terminalSplitRight.title", defaultValue: "Split Right") keywords = ["terminal", "split", "right"] @@ -1768,6 +1773,35 @@ struct CmuxSurfaceDefinition: Codable, Sendable { enum CmuxSurfaceType: String, Codable, Sendable { case terminal case browser + case codeEditor = "editor" + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + let rawValue = try container.decode(String.self) + let normalized = rawValue + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "-", with: "") + .replacingOccurrences(of: "_", with: "") + .lowercased() + switch normalized { + case Self.terminal.rawValue: + self = .terminal + case Self.browser.rawValue: + self = .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": + self = .codeEditor + default: + throw DecodingError.dataCorruptedError( + in: container, + debugDescription: "Unknown surface type: \(rawValue)" + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(rawValue) + } } struct CmuxResolvedCommand: Sendable { @@ -2198,6 +2232,7 @@ final class CmuxConfigStore: ObservableObject { }) ?? [ .builtIn(.newTerminal), .builtIn(.newBrowser), + .builtIn(.newCodeEditor), .builtIn(.splitRight), .builtIn(.splitDown) ] diff --git a/Sources/CmuxLifecycleEventPublishing.swift b/Sources/CmuxLifecycleEventPublishing.swift index 476598485200..05865d2d43b4 100644 --- a/Sources/CmuxLifecycleEventPublishing.swift +++ b/Sources/CmuxLifecycleEventPublishing.swift @@ -208,6 +208,8 @@ extension Workspace { return "terminal" case .browser: return "browser" + case .codeEditor: + return "editor" case .markdown: return "markdown" case .filePreview: diff --git a/Sources/CmuxSurfaceTabBarBuiltInAction.swift b/Sources/CmuxSurfaceTabBarBuiltInAction.swift index cc7586e14ea8..cdb031173001 100644 --- a/Sources/CmuxSurfaceTabBarBuiltInAction.swift +++ b/Sources/CmuxSurfaceTabBarBuiltInAction.swift @@ -6,6 +6,7 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha case cloudVM = "cmux.cloudvm" case newTerminal = "cmux.newTerminal" case newBrowser = "cmux.newBrowser" + case newCodeEditor = "cmux.newCodeEditor" case splitRight = "cmux.splitRight" case splitDown = "cmux.splitDown" @@ -21,6 +22,8 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha self = .newTerminal case "cmux.newBrowser", "newBrowser": self = .newBrowser + case "cmux.newCodeEditor", "cmux.newEditor", "newCodeEditor", "newEditor", "codeEditor", "editor": + self = .newCodeEditor case "cmux.splitRight", "splitRight": self = .splitRight case "cmux.splitDown", "splitDown": @@ -44,6 +47,8 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha return "terminal" case .newBrowser: return "globe" + case .newCodeEditor: + return "curlybraces.square" case .splitRight: return "square.split.2x1" case .splitDown: @@ -53,7 +58,7 @@ enum CmuxSurfaceTabBarBuiltInAction: String, Codable, Sendable, CaseIterable, Ha var bonsplitAction: BonsplitConfiguration.SplitActionButton.Action? { switch self { - case .newWorkspace, .cloudVM: + case .newWorkspace, .cloudVM, .newCodeEditor: return nil case .newTerminal: return .newTerminal diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 6348551f4816..069c5ed04b0a 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -5760,6 +5760,8 @@ struct ContentView: View { return String(localized: "commandPalette.kind.terminal", defaultValue: "Terminal") case .browser: return String(localized: "commandPalette.kind.browser", defaultValue: "Browser") + case .codeEditor: + return String(localized: "commandPalette.kind.codeEditor", defaultValue: "Code Editor") case .markdown: return String(localized: "commandPalette.kind.markdown", defaultValue: "Markdown") case .filePreview: @@ -5775,6 +5777,8 @@ struct ContentView: View { return ["terminal", "shell", "console"] case .browser: return ["browser", "web", "page"] + case .codeEditor: + return ["code", "editor", "vscode", "vs", "cursor"] case .markdown: return ["markdown", "note", "preview"] case .filePreview: @@ -6129,6 +6133,8 @@ struct ContentView: View { return CmuxSurfaceTabBarBuiltInAction.newTerminal.configID case "palette.newBrowserTab": return CmuxSurfaceTabBarBuiltInAction.newBrowser.configID + case "palette.newCodeEditorTab": + return CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID case "palette.terminalSplitRight": return CmuxSurfaceTabBarBuiltInAction.splitRight.configID case "palette.terminalSplitDown": @@ -6436,6 +6442,15 @@ struct ContentView: View { when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } ) ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.newCodeEditorTab", + title: constant(String(localized: "command.newCodeEditorTab.title", defaultValue: "New Tab (Code Editor)")), + subtitle: constant(String(localized: "command.newCodeEditorTab.subtitle", defaultValue: "Tab")), + keywords: ["new", "code", "editor", "tab", "vscode", "vs", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) contributions.append( CommandPaletteCommandContribution( commandId: "palette.closeTab", @@ -7048,6 +7063,24 @@ struct ContentView: View { } ) ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.codeEditorSplitRight", + title: constant(String(localized: "command.codeEditorSplitRight.title", defaultValue: "Split Code Editor Right")), + subtitle: constant(String(localized: "command.codeEditorSplitRight.subtitle", defaultValue: "Code Editor Layout")), + keywords: ["code", "editor", "split", "right", "vscode", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) + contributions.append( + CommandPaletteCommandContribution( + commandId: "palette.codeEditorSplitDown", + title: constant(String(localized: "command.codeEditorSplitDown.title", defaultValue: "Split Code Editor Down")), + subtitle: constant(String(localized: "command.codeEditorSplitDown.subtitle", defaultValue: "Code Editor Layout")), + keywords: ["code", "editor", "split", "down", "vscode", "cursor"], + when: { !$0.bool(CommandPaletteContextKeys.browserDisabled) } + ) + ) contributions.append( CommandPaletteCommandContribution( commandId: "palette.browserDuplicateRight", @@ -7489,6 +7522,12 @@ struct ContentView: View { _ = AppDelegate.shared?.openBrowserAndFocusAddressBar() } } + registry.register(commandId: "palette.newCodeEditorTab") { + if executeConfiguredAction(id: CmuxSurfaceTabBarBuiltInAction.newCodeEditor.configID) { + return + } + _ = AppDelegate.shared?.openCodeEditor(tabManager: tabManager) + } registry.register(commandId: "palette.closeTab") { tabManager.closeCurrentPanelWithConfirmation() } @@ -7811,6 +7850,12 @@ struct ContentView: View { registry.register(commandId: "palette.browserSplitDown") { _ = tabManager.createBrowserSplit(direction: .down) } + registry.register(commandId: "palette.codeEditorSplitRight") { + _ = tabManager.createCodeEditorSplit(direction: .right) + } + registry.register(commandId: "palette.codeEditorSplitDown") { + _ = tabManager.createCodeEditorSplit(direction: .down) + } registry.register(commandId: "palette.browserDuplicateRight") { let url = tabManager.focusedBrowserPanel?.preferredURLStringForOmnibar().flatMap(URL.init(string:)) _ = tabManager.createBrowserSplit(direction: .right, url: url) diff --git a/Sources/KeyboardShortcutContext.swift b/Sources/KeyboardShortcutContext.swift index 43e9d3f48482..6b04c39c6cbf 100644 --- a/Sources/KeyboardShortcutContext.swift +++ b/Sources/KeyboardShortcutContext.swift @@ -167,6 +167,7 @@ extension AppDelegate { for manager in shortcutCandidateTabManagers() { for workspace in manager.tabs { if let panel = workspace.browserPanel(for: panelId) { + guard panel.panelType == .browser else { continue } return panel } } @@ -179,6 +180,7 @@ extension AppDelegate { for workspace in manager.tabs { for panel in workspace.panels.values { guard let browserPanel = panel as? BrowserPanel, + browserPanel.panelType == .browser, browserPanel.webView === webView else { continue } diff --git a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift index 8fd68e27b82f..c49da482a481 100644 --- a/Sources/Panels/BrowserOmnibarPerformanceSupport.swift +++ b/Sources/Panels/BrowserOmnibarPerformanceSupport.swift @@ -193,7 +193,8 @@ extension TabManager { private func browserOpenTabSuggestionSeedSnapshots() -> [BrowserOpenTabSuggestionSnapshot] { tabs.flatMap { workspace in workspace.panels.compactMap { _, panel in - guard let browserPanel = panel as? BrowserPanel else { return nil } + guard let browserPanel = panel as? BrowserPanel, + browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions else { return nil } return BrowserOpenTabSuggestionSnapshot( workspaceId: workspace.id, panelId: browserPanel.id, @@ -207,6 +208,10 @@ extension TabManager { extension Workspace { func publishBrowserOpenTabSuggestion(for browserPanel: BrowserPanel) { + guard browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions else { + owningTabManager?.removeBrowserOpenTabSuggestion(panelId: browserPanel.id) + return + } guard let snapshot = BrowserOpenTabSuggestionSnapshot( workspaceId: id, panelId: browserPanel.id, diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 00ccf669bfa7..fe59757da00f 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -2465,8 +2465,66 @@ final class BrowserPanel: Panel, ObservableObject { return NSColor.windowBackgroundColor } + enum SurfaceRole: String, Codable, Sendable { + case browser + case codeEditor + + var panelType: PanelType { + switch self { + case .browser: + return .browser + case .codeEditor: + return .codeEditor + } + } + + var displayIcon: String { + switch self { + case .browser: + return "globe" + case .codeEditor: + return "curlybraces.square" + } + } + + var defaultDisplayTitle: String { + switch self { + case .browser: + return String(localized: "browser.newTab", defaultValue: "New tab") + case .codeEditor: + return String(localized: "codeEditor.newTab", defaultValue: "Code Editor") + } + } + + var defaultInitialURL: URL? { + switch self { + case .browser: + return nil + case .codeEditor: + return URL(string: "https://vscode.dev/") + } + } + + var showsBrowserChrome: Bool { + self == .browser + } + + var recordsBrowserHistory: Bool { + self == .browser + } + + var contributesToBrowserOpenTabSuggestions: Bool { + self == .browser + } + + var contributesToGlobalBrowserSearch: Bool { + self == .browser + } + } + let id: UUID - let panelType: PanelType = .browser + let surfaceRole: SurfaceRole + let panelType: PanelType /// The workspace ID this panel belongs to private(set) var workspaceId: UUID @@ -2925,9 +2983,14 @@ final class BrowserPanel: Panel, ObservableObject { return pageTitle } if let url = currentURL { + if surfaceRole == .codeEditor, + let defaultURL = SurfaceRole.codeEditor.defaultInitialURL, + url.absoluteString == defaultURL.absoluteString { + return surfaceRole.defaultDisplayTitle + } return url.host ?? url.absoluteString } - return String(localized: "browser.newTab", defaultValue: "New tab") + return surfaceRole.defaultDisplayTitle } var profileDisplayName: String { @@ -3343,7 +3406,7 @@ final class BrowserPanel: Panel, ObservableObject { } var displayIcon: String? { - "globe" + surfaceRole.displayIcon } var isDirty: Bool { @@ -3479,7 +3542,9 @@ final class BrowserPanel: Panel, ObservableObject { self.isMainFrameProvisionalNavigationActive = false self.publishCommittedURL(from: webView) self.realignRestoredSessionHistoryToLiveCurrentIfPossible() - boundHistoryStore.recordVisit(url: webView.url, title: webView.title) + if self.surfaceRole.recordsBrowserHistory { + boundHistoryStore.recordVisit(url: webView.url, title: webView.title) + } self.refreshFavicon(from: webView) // Keep find-in-page open through load completion and refresh matches for the new DOM. self.restoreFindStateAfterNavigation(replaySearch: true) @@ -3511,7 +3576,9 @@ final class BrowserPanel: Panel, ObservableObject { private func publishCommittedURL(from webView: WKWebView) { currentURL = Self.remoteProxyDisplayURL(for: webView.url) - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } private func isCurrentWebView(_ candidate: WKWebView, instanceID: UUID? = nil) -> Bool { @@ -3522,6 +3589,7 @@ final class BrowserPanel: Panel, ObservableObject { init( workspaceId: UUID, + surfaceRole: SurfaceRole = .browser, profileID: UUID? = nil, initialURL: URL? = nil, initialRequest: URLRequest? = nil, @@ -3533,6 +3601,8 @@ final class BrowserPanel: Panel, ObservableObject { remoteWebsiteDataStoreIdentifier: UUID? = nil ) { self.id = UUID() + self.surfaceRole = surfaceRole + self.panelType = surfaceRole.panelType self.workspaceId = workspaceId let requestedProfileID = profileID ?? BrowserProfileStore.shared.effectiveLastUsedProfileID let resolvedProfileID = BrowserProfileStore.shared.profileDefinition(id: requestedProfileID) != nil @@ -4207,7 +4277,9 @@ final class BrowserPanel: Panel, ObservableObject { guard let self, self.isCurrentWebView(webView, instanceID: observedWebViewInstanceID) else { return } guard !self.isMainFrameProvisionalNavigationActive else { return } self.currentURL = Self.remoteProxyDisplayURL(for: observedURL) - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if self.surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } } webViewObservers.append(urlObserver) @@ -4222,7 +4294,9 @@ final class BrowserPanel: Panel, ObservableObject { let trimmed = (webView.title ?? "").trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } self.pageTitle = trimmed - GlobalSearchCoordinator.shared.captureBrowserPanel(self) + if self.surfaceRole.contributesToGlobalBrowserSearch { + GlobalSearchCoordinator.shared.captureBrowserPanel(self) + } } } webViewObservers.append(titleObserver) @@ -5505,7 +5579,8 @@ extension BrowserPanel { initialRequest: seed.initialRequest, focus: true, preferredProfileID: profileID, - bypassInsecureHTTPHostOnce: seed.bypassInsecureHTTPHostOnce + bypassInsecureHTTPHostOnce: seed.bypassInsecureHTTPHostOnce, + surfaceRole: surfaceRole ) else { #if DEBUG cmuxDebugLog("browser.newTab.open.abort panel=\(id.uuidString.prefix(5)) reason=newPanelFailed") diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 2befd1a8eb28..b00f423b5d28 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -710,8 +710,10 @@ struct BrowserPanelView: View { // Layering contract: browser find UI is mounted in the portal-hosted AppKit // container. Rendering it here can hide it behind the portal-hosted WKWebView. VStack(spacing: 0) { - addressBar - .fixedSize(horizontal: false, vertical: true) + if panel.surfaceRole.showsBrowserChrome { + addressBar + .fixedSize(horizontal: false, vertical: true) + } webView } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) @@ -770,6 +772,12 @@ struct BrowserPanelView: View { .onPreferenceChange(BrowserAddressBarHeightPreferenceKey.self) { height in addressBarHeight = height } + .onChange(of: panel.surfaceRole.showsBrowserChrome) { _, showsBrowserChrome in + if !showsBrowserChrome { + addressBarHeight = 0 + omnibarPillFrame = .zero + } + } .onReceive(NotificationCenter.default.publisher(for: .webViewDidReceiveClick).filter { [weak panel] note in // Only handle clicks from our own webview. guard let webView = note.object as? CmuxWebView else { return false } diff --git a/Sources/Panels/Panel.swift b/Sources/Panels/Panel.swift index 97075078d8d4..1c75585f5b85 100644 --- a/Sources/Panels/Panel.swift +++ b/Sources/Panels/Panel.swift @@ -6,25 +6,42 @@ import AppKit public enum PanelType: String, Codable, Sendable { case terminal case browser + case codeEditor = "editor" case markdown case filePreview = "filepreview" case rightSidebarTool + public init?(externalValue rawValue: String) { + let normalized = rawValue + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "-", with: "") + .replacingOccurrences(of: "_", with: "") + .lowercased() + switch normalized { + case Self.terminal.rawValue: + self = .terminal + case Self.browser.rawValue: + self = .browser + case "editor", "code", "codeeditor", "vscode", "vscodeinline": + self = .codeEditor + case Self.markdown.rawValue: + self = .markdown + case Self.filePreview.rawValue: + self = .filePreview + case "rightsidebartool": + self = .rightSidebarTool + default: + return nil + } + } + public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() let rawValue = try container.decode(String.self) - if let type = Self(rawValue: rawValue) { + if let type = Self(externalValue: rawValue) { self = type return } - if rawValue.lowercased() == Self.filePreview.rawValue { - self = .filePreview - return - } - if rawValue.lowercased() == Self.rightSidebarTool.rawValue.lowercased() { - self = .rightSidebarTool - return - } throw DecodingError.dataCorruptedError( in: container, debugDescription: "Unknown panel type: \(rawValue)" diff --git a/Sources/Panels/PanelContentView.swift b/Sources/Panels/PanelContentView.swift index 568c1dffd16a..7d1315b2e7b6 100644 --- a/Sources/Panels/PanelContentView.swift +++ b/Sources/Panels/PanelContentView.swift @@ -46,7 +46,7 @@ struct PanelContentView: View { onTriggerFlash: onTriggerFlash ) } - case .browser: + case .browser, .codeEditor: if let browserPanel = panel as? BrowserPanel { BrowserPanelView( panel: browserPanel, @@ -108,7 +108,7 @@ struct PanelContentView: View { switch panel.panelType { case .markdown, .filePreview, .rightSidebarTool: return true - case .terminal, .browser: + case .terminal, .browser, .codeEditor: return false } } diff --git a/Sources/Search/GlobalSearchDocuments.swift b/Sources/Search/GlobalSearchDocuments.swift index baa92275e4fc..c47c4233ea43 100644 --- a/Sources/Search/GlobalSearchDocuments.swift +++ b/Sources/Search/GlobalSearchDocuments.swift @@ -34,7 +34,7 @@ enum GlobalSearchDocuments { kind = .browser case .markdown: kind = .markdown - case .terminal, .filePreview, .rightSidebarTool: + case .terminal, .codeEditor, .filePreview, .rightSidebarTool: kind = .title } diff --git a/Sources/Search/GlobalSearchPanelCaptureManager.swift b/Sources/Search/GlobalSearchPanelCaptureManager.swift index 41e5d1343eca..4362cbe7ab98 100644 --- a/Sources/Search/GlobalSearchPanelCaptureManager.swift +++ b/Sources/Search/GlobalSearchPanelCaptureManager.swift @@ -36,12 +36,18 @@ final class GlobalSearchPanelCaptureManager { #endif } } - } else if let browserPanel = context.panel as? BrowserPanel { + } else if let browserPanel = context.panel as? BrowserPanel, + browserPanel.surfaceRole.contributesToGlobalBrowserSearch { captureBrowserPanel(browserPanel) } } func captureBrowserPanel(_ panel: BrowserPanel) { + guard panel.surfaceRole.contributesToGlobalBrowserSearch else { + cancelBrowserCapture(forPanelID: panel.id) + return + } + let panelID = panel.id let taskID = UUID() cancelPanelPurge(panelID) @@ -202,6 +208,7 @@ final class GlobalSearchPanelCaptureManager { } private func indexBrowserPanel(_ panel: BrowserPanel) async { + guard panel.surfaceRole.contributesToGlobalBrowserSearch else { return } guard let context = AppDelegate.shared?.globalSearchContext( forPanelID: panel.id, preferredWorkspaceID: panel.workspaceId diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 42543ea1339d..ecdd7bcd06b1 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -6486,8 +6486,10 @@ class TabManager: ObservableObject { /// Returns the focused panel if it's a BrowserPanel, nil otherwise var focusedBrowserPanel: BrowserPanel? { guard let tab = selectedWorkspace, - let panelId = tab.focusedPanelId else { return nil } - return tab.panels[panelId] as? BrowserPanel + let panelId = tab.focusedPanelId, + let panel = tab.panels[panelId] as? BrowserPanel, + panel.panelType == .browser else { return nil } + return panel } @discardableResult @@ -7299,6 +7301,23 @@ class TabManager: ObservableObject { ) } + /// Create a new code editor split from the currently focused panel. + @discardableResult + func createCodeEditorSplit(direction: SplitDirection, url: URL? = nil) -> UUID? { + guard let selectedTabId, + let tab = tabs.first(where: { $0.id == selectedTabId }), + let focusedPanelId = tab.focusedPanelId else { return nil } + tab.clearSplitZoom() + return newBrowserSplit( + tabId: selectedTabId, + fromPanelId: focusedPanelId, + orientation: direction.orientation, + insertFirst: direction.insertFirst, + url: url, + surfaceRole: .codeEditor + ) + } + /// Refresh Bonsplit right-side action button tooltips for all workspaces. func refreshSplitButtonTooltips() { for workspace in tabs { @@ -7584,6 +7603,7 @@ class TabManager: ObservableObject { url: URL? = nil, preferredProfileID: UUID? = nil, focus: Bool = true, + surfaceRole: BrowserPanel.SurfaceRole = .browser, initialDividerPosition: CGFloat? = nil ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } @@ -7595,40 +7615,80 @@ class TabManager: ObservableObject { url: url, preferredProfileID: preferredProfileID, focus: focus, + surfaceRole: surfaceRole, initialDividerPosition: initialDividerPosition )?.id } + func newCodeEditorSplit( + tabId: UUID, + fromPanelId: UUID, + orientation: SplitOrientation, + insertFirst: Bool = false, + url: URL? = nil, + preferredProfileID: UUID? = nil, + focus: Bool = true, + initialDividerPosition: CGFloat? = nil + ) -> UUID? { + newBrowserSplit( + tabId: tabId, + fromPanelId: fromPanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + preferredProfileID: preferredProfileID, + focus: focus, + surfaceRole: .codeEditor, + initialDividerPosition: initialDividerPosition + ) + } + /// Create a new browser surface in a pane func newBrowserSurface( tabId: UUID, inPane paneId: PaneID, url: URL? = nil, - preferredProfileID: UUID? = nil + preferredProfileID: UUID? = nil, + surfaceRole: BrowserPanel.SurfaceRole = .browser ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil } return tab.newBrowserSurface( inPane: paneId, url: url, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole )?.id } + func newCodeEditorSurface( + tabId: UUID, + inPane paneId: PaneID, + url: URL? = nil, + preferredProfileID: UUID? = nil + ) -> UUID? { + newBrowserSurface( + tabId: tabId, + inPane: paneId, + url: url, + preferredProfileID: preferredProfileID, + surfaceRole: .codeEditor + ) + } + /// Get a browser panel by ID func browserPanel(tabId: UUID, panelId: UUID) -> BrowserPanel? { guard let tab = tabs.first(where: { $0.id == tabId }) else { return nil } return tab.browserPanel(for: panelId) } - /// Open a browser in a specific workspace, optionally preferring a split-right layout. - @discardableResult - func openBrowser( + private func openBrowserSurfaceRole( + _ surfaceRole: BrowserPanel.SurfaceRole, inWorkspace tabId: UUID, - url: URL? = nil, - preferSplitRight: Bool = false, - preferredProfileID: UUID? = nil, - insertAtEnd: Bool = false + url: URL?, + preferSplitRight: Bool, + preferredProfileID: UUID?, + insertAtEnd: Bool ) -> UUID? { guard BrowserAvailabilitySettings.isEnabled() else { return nil } guard let workspace = tabs.first(where: { $0.id == tabId }) else { return nil } @@ -7638,15 +7698,16 @@ class TabManager: ObservableObject { if preferSplitRight { if let targetPaneId = workspace.topRightBrowserReusePane(), - let browserPanel = workspace.newBrowserSurface( + let panel = workspace.newBrowserSurface( inPane: targetPaneId, url: url, focus: true, insertAtEnd: insertAtEnd, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole ) { - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id } let splitSourcePanelId: UUID? = { @@ -7665,30 +7726,70 @@ class TabManager: ObservableObject { }() if let splitSourcePanelId, - let browserPanel = workspace.newBrowserSplit( + let panel = workspace.newBrowserSplit( from: splitSourcePanelId, orientation: .horizontal, url: url, preferredProfileID: preferredProfileID, - focus: true + focus: true, + surfaceRole: surfaceRole ) { - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id } } guard let paneId = workspace.bonsplitController.focusedPaneId ?? workspace.bonsplitController.allPaneIds.first, - let browserPanel = workspace.newBrowserSurface( + let panel = workspace.newBrowserSurface( inPane: paneId, url: url, focus: true, insertAtEnd: insertAtEnd, - preferredProfileID: preferredProfileID + preferredProfileID: preferredProfileID, + surfaceRole: surfaceRole ) else { return nil } - rememberFocusedSurface(tabId: tabId, surfaceId: browserPanel.id) - return browserPanel.id + rememberFocusedSurface(tabId: tabId, surfaceId: panel.id) + return panel.id + } + + /// Open a browser in a specific workspace, optionally preferring a split-right layout. + @discardableResult + func openBrowser( + inWorkspace tabId: UUID, + url: URL? = nil, + preferSplitRight: Bool = false, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + openBrowserSurfaceRole( + .browser, + inWorkspace: tabId, + url: url, + preferSplitRight: preferSplitRight, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) + } + + /// Open a code editor in a specific workspace, optionally preferring a split-right layout. + @discardableResult + func openCodeEditor( + inWorkspace tabId: UUID, + url: URL? = nil, + preferSplitRight: Bool = false, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + openBrowserSurfaceRole( + .codeEditor, + inWorkspace: tabId, + url: url, + preferSplitRight: preferSplitRight, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) } /// Open a browser in the currently focused pane (as a new surface) @@ -7708,6 +7809,23 @@ class TabManager: ObservableObject { ) } + /// Open a code editor in the currently focused pane (as a new surface). + @discardableResult + func openCodeEditor( + url: URL? = nil, + preferredProfileID: UUID? = nil, + insertAtEnd: Bool = false + ) -> UUID? { + guard let tabId = selectedTabId else { return nil } + return openCodeEditor( + inWorkspace: tabId, + url: url, + preferSplitRight: false, + preferredProfileID: preferredProfileID, + insertAtEnd: insertAtEnd + ) + } + /// Reopen the most recently closed browser panel (Cmd+Shift+T). /// No-op when no browser panel restore snapshot is available. @discardableResult diff --git a/Sources/TaskManagerSnapshot.swift b/Sources/TaskManagerSnapshot.swift index b85e302d0fff..3045ea077812 100644 --- a/Sources/TaskManagerSnapshot.swift +++ b/Sources/TaskManagerSnapshot.swift @@ -446,6 +446,16 @@ struct CmuxTaskManagerSnapshot { let title = nonEmptyString(surface["title"]) ?? displayHandle(surface) let surfaceId = uuid(surface["id"]) let terminalSurfaceId = type == "terminal" ? surfaceId : nil + let surfaceKind: CmuxTaskManagerRow.Kind = { + switch type { + case "browser": + return .browserSurface + case "editor", "code_editor", "codeeditor": + return .codeEditorSurface + default: + return .terminalSurface + } + }() var detailParts = [surfaceTypeLabel(type)] if bool(surface["selected"]) { detailParts.append(String(localized: "taskManager.row.selected", defaultValue: "Selected")) @@ -458,7 +468,7 @@ struct CmuxTaskManagerSnapshot { } rows.append(row( surface, - kind: type == "browser" ? .browserSurface : .terminalSurface, + kind: surfaceKind, level: 3, title: title, detail: detailParts.joined(separator: " / "), @@ -475,7 +485,7 @@ struct CmuxTaskManagerSnapshot { } } let processes = surface["processes"] as? [[String: Any]] ?? [] - let context = rowID(surface, kind: type == "browser" ? .browserSurface : .terminalSurface) + let context = rowID(surface, kind: surfaceKind) for process in processes { appendProcess( process, @@ -655,6 +665,8 @@ struct CmuxTaskManagerSnapshot { switch type { case "browser": return String(localized: "taskManager.row.surfaceType.browser", defaultValue: "Browser") + case "editor", "code_editor", "codeeditor": + return String(localized: "taskManager.row.surfaceType.codeEditor", defaultValue: "Code Editor") case "terminal": return String(localized: "taskManager.row.surfaceType.terminal", defaultValue: "Terminal") case "unknown", "": diff --git a/Sources/TaskManagerTypes.swift b/Sources/TaskManagerTypes.swift index c99a802d3285..58be137f069b 100644 --- a/Sources/TaskManagerTypes.swift +++ b/Sources/TaskManagerTypes.swift @@ -10,6 +10,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case pane case terminalSurface case browserSurface + case codeEditorSurface case webview case process case programAggregate @@ -24,6 +25,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case .pane: return "square.split.2x1" case .terminalSurface: return "terminal" case .browserSurface: return "globe" + case .codeEditorSurface: return "curlybraces.square" case .webview: return "network" case .process: return "gearshape" case .programAggregate: return "gearshape.2" @@ -40,6 +42,7 @@ struct CmuxTaskManagerRow: Identifiable, Equatable { case .pane: return .secondary case .terminalSurface: return .green case .browserSurface: return .blue + case .codeEditorSurface: return .teal case .webview: return .purple case .process: return .secondary case .programAggregate: return .accentColor diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 17c5d98be24b..8638f096b172 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -8348,7 +8348,7 @@ class TerminalController { return error } let initialDividerPosition = parsedInitialDivider.value - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return v2BrowserDisabledExternalOpenResult(rawURL: urlStr, url: url, tabManager: tabManager) } @@ -8391,6 +8391,16 @@ class TerminalController { creationPolicy: .automationPreload, initialDividerPosition: initialDividerPosition.map { CGFloat($0) } )?.id + } else if panelType == .codeEditor { + newId = ws.newCodeEditorSplit( + from: targetSurfaceId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload, + initialDividerPosition: initialDividerPosition.map { CGFloat($0) } + )?.id } else { newId = tabManager.newSplit( tabId: ws.id, @@ -8439,7 +8449,7 @@ class TerminalController { let tmuxStartCommand = v2OptionalTrimmedRawString(params, "tmux_start_command") let remotePTYSessionID = v2OptionalTrimmedRawString(params, "remote_pty_session_id") let startupEnvironment = v2TrimmedStringMap(params, keys: ["startup_environment", "initial_env"]) - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return v2BrowserDisabledExternalOpenResult(rawURL: urlStr, url: url, tabManager: tabManager) } @@ -8474,6 +8484,13 @@ class TerminalController { focus: focus, creationPolicy: .automationPreload )?.id + } else if panelType == .codeEditor { + newPanelId = ws.newCodeEditorSurface( + inPane: paneId, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id } else { newPanelId = ws.newTerminalSurface( inPane: paneId, @@ -9756,7 +9773,7 @@ class TerminalController { let initialCommand = v2OptionalTrimmedRawString(params, "initial_command") let tmuxStartCommand = v2OptionalTrimmedRawString(params, "tmux_start_command") let startupEnvironment = v2TrimmedStringMap(params, keys: ["startup_environment", "initial_env"]) - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return v2BrowserDisabledExternalOpenResult(rawURL: urlStr, url: url, tabManager: tabManager) } @@ -9795,6 +9812,16 @@ class TerminalController { creationPolicy: .automationPreload, initialDividerPosition: initialDividerPosition.map { CGFloat($0) } )?.id + } else if panelType == .codeEditor { + newPanelId = ws.newCodeEditorSplit( + from: sourcePanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload, + initialDividerPosition: initialDividerPosition.map { CGFloat($0) } + )?.id } else { newPanelId = ws.newTerminalSplit( from: sourcePanelId, @@ -18710,7 +18737,7 @@ class TerminalController { let partStr = String(part) if partStr.hasPrefix("--type=") { let typeStr = String(partStr.dropFirst(7)) - panelType = typeStr == "browser" ? .browser : .terminal + panelType = PanelType(externalValue: typeStr) ?? .terminal } else if partStr.hasPrefix("--direction=") { let dirStr = String(partStr.dropFirst(12)) if let parsed = parseSplitDirection(dirStr) { @@ -18728,7 +18755,7 @@ class TerminalController { if invalidDirection { return "ERROR: Invalid direction. Use left, right, up, or down." } - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return openExternallyWhenBrowserDisabled(rawURL: urlRaw, url: url) } @@ -18754,6 +18781,15 @@ class TerminalController { focus: focus, creationPolicy: .automationPreload )?.id + } else if panelType == .codeEditor { + newPanelId = tab.newCodeEditorSplit( + from: focusedPanelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id } else { newPanelId = tab.newTerminalSplit( from: focusedPanelId, @@ -20373,7 +20409,7 @@ class TerminalController { let partStr = String(part) if partStr.hasPrefix("--type=") { let typeStr = String(partStr.dropFirst(7)) - panelType = typeStr == "browser" ? .browser : .terminal + panelType = PanelType(externalValue: typeStr) ?? .terminal } else if partStr.hasPrefix("--pane=") { paneArg = String(partStr.dropFirst(7)) } else if partStr.hasPrefix("--url=") { @@ -20382,7 +20418,7 @@ class TerminalController { url = urlRaw.flatMap { URL(string: $0) } } } - if panelType == .browser, BrowserAvailabilitySettings.isDisabled() { + if (panelType == .browser || panelType == .codeEditor), BrowserAvailabilitySettings.isDisabled() { return openExternallyWhenBrowserDisabled(rawURL: urlRaw, url: url) } @@ -20422,6 +20458,13 @@ class TerminalController { focus: focus, creationPolicy: .automationPreload )?.id + } else if panelType == .codeEditor { + newPanelId = tab.newCodeEditorSurface( + inPane: targetPaneId, + url: url, + focus: focus, + creationPolicy: .automationPreload + )?.id } else { newPanelId = tab.newTerminalSurface(inPane: targetPaneId, focus: focus)?.id } diff --git a/Sources/TerminalControllerV2ParamParsingSupport.swift b/Sources/TerminalControllerV2ParamParsingSupport.swift index 1bb365e53ce4..d9d12785352c 100644 --- a/Sources/TerminalControllerV2ParamParsingSupport.swift +++ b/Sources/TerminalControllerV2ParamParsingSupport.swift @@ -183,9 +183,6 @@ extension TerminalController { func v2PanelType(_ params: [String: Any], _ key: String) -> PanelType? { guard let s = v2String(params, key) else { return nil } - let normalized = s.replacingOccurrences(of: "-", with: "") - .replacingOccurrences(of: "_", with: "") - .lowercased() - return PanelType(rawValue: normalized) + return PanelType(externalValue: s) } } diff --git a/Sources/TerminalPaneDropTargetView.swift b/Sources/TerminalPaneDropTargetView.swift index 76baedebdd8f..fe7498775062 100644 --- a/Sources/TerminalPaneDropTargetView.swift +++ b/Sources/TerminalPaneDropTargetView.swift @@ -328,7 +328,7 @@ final class PaneDropTargetView: NSView { switch panel.panelType { case .terminal: return .terminal - case .browser: + case .browser, .codeEditor: return nil case .filePreview: guard let filePreviewPanel = panel as? FilePreviewPanel, diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 322856d16b6f..e9778f9a18b6 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -544,7 +544,7 @@ extension Workspace { markdownSnapshot = nil filePreviewSnapshot = nil rightSidebarToolSnapshot = nil - case .browser: + case .browser, .codeEditor: guard let browserPanel = panel as? BrowserPanel else { return nil } terminalSnapshot = nil let historySnapshot = browserPanel.sessionNavigationHistorySnapshot() @@ -1057,6 +1057,18 @@ extension Workspace { } applySessionPanelMetadata(snapshot, toPanelId: browserPanel.id) return browserPanel.id + case .codeEditor: + guard let editorPanel = newCodeEditorSurface( + inPane: paneId, + url: nil, + focus: false, + preferredProfileID: snapshot.browser?.profileID, + creationPolicy: .restoration + ) else { + return nil + } + applySessionPanelMetadata(snapshot, toPanelId: editorPanel.id) + return editorPanel.id case .markdown: guard let filePath = snapshot.markdown?.filePath, let markdownPanel = newMarkdownSurface( @@ -1362,6 +1374,19 @@ extension Workspace { if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } if surface.focus == true { focusPanelId = panel.id } } + + case .codeEditor: + let url = surface.url.flatMap { URL(string: $0) } + if let panel = newCodeEditorSurface( + inPane: paneId, + url: url ?? BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + focus: false, + creationPolicy: .restoration + ) { + _ = closePanel(panelId, force: true) + if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } + if surface.focus == true { focusPanelId = panel.id } + } } } @@ -1396,6 +1421,18 @@ extension Workspace { if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } if surface.focus == true { focusPanelId = panel.id } } + + case .codeEditor: + let url = surface.url.flatMap { URL(string: $0) } + if let panel = newCodeEditorSurface( + inPane: paneId, + url: url ?? BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, + focus: false, + creationPolicy: .restoration + ) { + if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } + if surface.focus == true { focusPanelId = panel.id } + } } } @@ -9352,6 +9389,7 @@ final class Workspace: Identifiable, ObservableObject { enum SurfaceKind { static let terminal = "terminal" static let browser = "browser" + static let codeEditor = "editor" static let markdown = "markdown" static let filePreview = "filePreview" static let rightSidebarTool = "rightSidebarTool" @@ -10061,7 +10099,9 @@ final class Workspace: Identifiable, ObservableObject { guard let self = self, let browserPanel = browserPanel, let tabId = self.surfaceIdFromPanelId(browserPanel.id) else { return } - self.publishBrowserOpenTabSuggestion(for: browserPanel) + if browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions { + self.publishBrowserOpenTabSuggestion(for: browserPanel) + } guard let existing = self.bonsplitController.tab(tabId) else { return } let nextTitle = browserPanel.displayTitle if self.panelTitles[browserPanel.id] != nextTitle { @@ -10081,7 +10121,9 @@ final class Workspace: Identifiable, ObservableObject { ) } panelSubscriptions[browserPanel.id] = subscription - publishBrowserOpenTabSuggestion(for: browserPanel) + if browserPanel.surfaceRole.contributesToBrowserOpenTabSuggestions { + publishBrowserOpenTabSuggestion(for: browserPanel) + } setPreferredBrowserProfileID(browserPanel.profileID) } @@ -10114,6 +10156,47 @@ final class Workspace: Identifiable, ObservableObject { return BrowserProfileStore.shared.effectiveLastUsedProfileID } + private func browserSurfaceKind(for role: BrowserPanel.SurfaceRole) -> String { + switch role { + case .browser: + return SurfaceKind.browser + case .codeEditor: + return SurfaceKind.codeEditor + } + } + + private func browserLifecycleKind(for role: BrowserPanel.SurfaceRole) -> String { + switch role { + case .browser: + return "browser" + case .codeEditor: + return "editor" + } + } + + private func browserLifecycleOrigin(for role: BrowserPanel.SurfaceRole, suffix: String) -> String { + switch role { + case .browser: + return "browser_\(suffix)" + case .codeEditor: + return "editor_\(suffix)" + } + } + + private func initialURLForBrowserRole( + _ role: BrowserPanel.SurfaceRole, + requestedURL: URL?, + creationPolicy: BrowserPanelCreationPolicy + ) -> URL? { + if let requestedURL { + return requestedURL + } + guard creationPolicy != .restoration else { + return nil + } + return role.defaultInitialURL + } + private func installMarkdownPanelSubscription(_ markdownPanel: MarkdownPanel) { let subscription = Publishers.CombineLatest( markdownPanel.$displayTitle.removeDuplicates(), @@ -10226,6 +10309,8 @@ final class Workspace: Identifiable, ObservableObject { return SurfaceKind.terminal case .browser: return SurfaceKind.browser + case .codeEditor: + return SurfaceKind.codeEditor case .markdown: return SurfaceKind.markdown case .filePreview: @@ -12621,13 +12706,14 @@ final class Workspace: Identifiable, ObservableObject { url: URL? = nil, preferredProfileID: UUID? = nil, focus: Bool = true, + surfaceRole: BrowserPanel.SurfaceRole = .browser, creationPolicy: BrowserPanelCreationPolicy = .userInitiated, initialDividerPosition: CGFloat? = nil ) -> BrowserPanel? { let browserEnabled = BrowserAvailabilitySettings.isEnabled() guard browserEnabled || creationPolicy.permitsCreationWhenBrowserDisabled else { - if let url { - _ = NSWorkspace.shared.open(url) + if let externalURL = initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy) { + _ = NSWorkspace.shared.open(externalURL) } return nil } @@ -12648,11 +12734,12 @@ final class Workspace: Identifiable, ObservableObject { // Create browser panel let browserPanel = BrowserPanel( workspaceId: id, + surfaceRole: surfaceRole, profileID: resolvedNewBrowserProfileID( preferredProfileID: preferredProfileID, sourcePanelId: panelId ), - initialURL: url, + initialURL: initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy), renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, proxyEndpoint: remoteProxyEndpoint, @@ -12667,7 +12754,7 @@ final class Workspace: Identifiable, ObservableObject { let newTab = Bonsplit.Tab( title: browserPanel.displayTitle, icon: browserPanel.displayIcon, - kind: SurfaceKind.browser, + kind: browserSurfaceKind(for: surfaceRole), isDirty: browserPanel.isDirty, isLoading: browserPanel.isLoading, isPinned: false @@ -12687,7 +12774,15 @@ final class Workspace: Identifiable, ObservableObject { } applyInitialSplitDividerPosition(initialDividerPosition, sourcePaneId: paneId, newPaneId: newPaneId) setPreferredBrowserProfileID(browserPanel.profileID) - publishCmuxSplitCreated(newPaneId, sourcePaneId: paneId, orientation: orientation, surfaceId: browserPanel.id, kind: "browser", origin: "browser_split", focused: focus) + publishCmuxSplitCreated( + newPaneId, + sourcePaneId: paneId, + orientation: orientation, + surfaceId: browserPanel.id, + kind: browserLifecycleKind(for: surfaceRole), + origin: browserLifecycleOrigin(for: surfaceRole, suffix: "split"), + focused: focus + ) // See newTerminalSplit: suppress old view's becomeFirstResponder during reparenting. let previousHostedView = focusedTerminalPanel?.hostedView @@ -12724,11 +12819,13 @@ final class Workspace: Identifiable, ObservableObject { insertAtEnd: Bool = false, preferredProfileID: UUID? = nil, bypassInsecureHTTPHostOnce: String? = nil, + surfaceRole: BrowserPanel.SurfaceRole = .browser, creationPolicy: BrowserPanelCreationPolicy = .userInitiated ) -> BrowserPanel? { let browserEnabled = BrowserAvailabilitySettings.isEnabled() guard browserEnabled || creationPolicy.permitsCreationWhenBrowserDisabled else { - if let externalURL = url ?? initialRequest?.url { + if let externalURL = initialRequest?.url + ?? initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy) { _ = NSWorkspace.shared.open(externalURL) } return nil @@ -12741,11 +12838,12 @@ final class Workspace: Identifiable, ObservableObject { let browserPanel = BrowserPanel( workspaceId: id, + surfaceRole: surfaceRole, profileID: resolvedNewBrowserProfileID( preferredProfileID: preferredProfileID, sourcePanelId: sourcePanelId ), - initialURL: url, + initialURL: initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy), initialRequest: initialRequest, renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, @@ -12761,7 +12859,7 @@ final class Workspace: Identifiable, ObservableObject { guard let newTabId = bonsplitController.createTab( title: browserPanel.displayTitle, icon: browserPanel.displayIcon, - kind: SurfaceKind.browser, + kind: browserSurfaceKind(for: surfaceRole), isDirty: browserPanel.isDirty, isLoading: browserPanel.isLoading, isPinned: false, @@ -12780,7 +12878,13 @@ final class Workspace: Identifiable, ObservableObject { let targetIndex = max(0, bonsplitController.tabs(inPane: paneId).count - 1) _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) } - publishCmuxSurfaceCreated(browserPanel.id, paneId: paneId, kind: "browser", origin: "browser_tab", focused: shouldFocusNewTab) + publishCmuxSurfaceCreated( + browserPanel.id, + paneId: paneId, + kind: browserLifecycleKind(for: surfaceRole), + origin: browserLifecycleOrigin(for: surfaceRole, suffix: "tab"), + focused: shouldFocusNewTab + ) // Match terminal behavior: enforce deterministic selection + focus. if shouldFocusNewTab { @@ -12802,6 +12906,50 @@ final class Workspace: Identifiable, ObservableObject { return browserPanel } + @discardableResult + func newCodeEditorSplit( + from panelId: UUID, + orientation: SplitOrientation, + insertFirst: Bool = false, + url: URL? = nil, + preferredProfileID: UUID? = nil, + focus: Bool = true, + creationPolicy: BrowserPanelCreationPolicy = .userInitiated, + initialDividerPosition: CGFloat? = nil + ) -> BrowserPanel? { + newBrowserSplit( + from: panelId, + orientation: orientation, + insertFirst: insertFirst, + url: url, + preferredProfileID: preferredProfileID, + focus: focus, + surfaceRole: .codeEditor, + creationPolicy: creationPolicy, + initialDividerPosition: initialDividerPosition + ) + } + + @discardableResult + func newCodeEditorSurface( + inPane paneId: PaneID, + url: URL? = nil, + focus: Bool? = nil, + insertAtEnd: Bool = false, + preferredProfileID: UUID? = nil, + creationPolicy: BrowserPanelCreationPolicy = .userInitiated + ) -> BrowserPanel? { + newBrowserSurface( + inPane: paneId, + url: url, + focus: focus, + insertAtEnd: insertAtEnd, + preferredProfileID: preferredProfileID, + surfaceRole: .codeEditor, + creationPolicy: creationPolicy + ) + } + /// Open the markdown viewer for `filePath`, reusing an existing /// `MarkdownPanel` in this workspace that already shows the same file. /// Paths are compared after symlink resolution so `./README.md` and a @@ -13530,6 +13678,7 @@ final class Workspace: Identifiable, ObservableObject { private func stageClosedBrowserRestoreSnapshotIfNeeded(for tab: Bonsplit.Tab, inPane pane: PaneID) { guard let panelId = panelIdFromSurfaceId(tab.id), let browserPanel = browserPanel(for: panelId), + browserPanel.panelType == .browser, let tabIndex = bonsplitController.tabs(inPane: pane).firstIndex(where: { $0.id == tab.id }) else { pendingClosedBrowserRestoreSnapshots.removeValue(forKey: tab.id) return @@ -14146,6 +14295,7 @@ final class Workspace: Identifiable, ObservableObject { trigger: FocusPanelTrigger ) { guard trigger == .standard else { return } + guard browserPanel.surfaceRole.showsBrowserChrome else { return } guard !isCommandPaletteVisibleForWorkspaceWindow() else { return } guard !browserPanel.shouldSuppressOmnibarAutofocus() else { return } guard browserPanel.isShowingNewTabPage || browserPanel.preferredURLStringForOmnibar() == nil else { return } @@ -16781,6 +16931,9 @@ extension Workspace: BonsplitDelegate { preferredWindow: presentingWindow, debugSource: "surfaceTabBar.cloudVM" ) + case .newCodeEditor: + bonsplitController.focusPane(pane) + _ = newCodeEditorSurface(inPane: pane, focus: true) case .newTerminal, .newBrowser, .splitRight, .splitDown: break } @@ -16859,6 +17012,8 @@ extension Workspace: BonsplitDelegate { _ = newTerminalSurface(inPane: pane) case "browser": _ = newBrowserSurface(inPane: pane) + case SurfaceKind.codeEditor, "codeEditor", "code_editor": + _ = newCodeEditorSurface(inPane: pane) default: _ = newTerminalSurface(inPane: pane) } diff --git a/Sources/WorkspaceContentView.swift b/Sources/WorkspaceContentView.swift index 0900ebd1c00f..2b11228c800d 100644 --- a/Sources/WorkspaceContentView.swift +++ b/Sources/WorkspaceContentView.swift @@ -770,6 +770,14 @@ struct EmptyPanelView: View { _ = workspace.newBrowserSurface(inPane: paneId) } + private func createCodeEditor() { + #if DEBUG + cmuxDebugLog("emptyPane.newCodeEditor pane=\(paneId.id.uuidString.prefix(5))") + #endif + focusPane() + _ = workspace.newCodeEditorSurface(inPane: paneId) + } + private var newSurfaceShortcut: StoredShortcut { let _ = keyboardShortcutSettingsObserver.revision return KeyboardShortcutSettings.shortcut(for: .newSurface) @@ -784,10 +792,10 @@ struct EmptyPanelView: View { private func emptyPaneActionButton( title: String, systemImage: String, - shortcut: StoredShortcut, + shortcut: StoredShortcut?, action: @escaping () -> Void ) -> some View { - if let key = shortcut.keyEquivalent { + if let shortcut, let key = shortcut.keyEquivalent { Button(action: action) { HStack(spacing: 10) { Label(title, systemImage: systemImage) @@ -800,7 +808,9 @@ struct EmptyPanelView: View { Button(action: action) { HStack(spacing: 10) { Label(title, systemImage: systemImage) - ShortcutHint(text: shortcut.displayString) + if let shortcut { + ShortcutHint(text: shortcut.displayString) + } } } .buttonStyle(.borderedProminent) @@ -813,24 +823,31 @@ struct EmptyPanelView: View { .font(.system(size: 48)) .foregroundStyle(.tertiary) - Text("Empty Panel") + Text(String(localized: "emptyPane.title", defaultValue: "Empty Panel")) .font(.headline) .foregroundStyle(.secondary) HStack(spacing: 12) { emptyPaneActionButton( - title: "Terminal", + title: String(localized: "emptyPane.terminal", defaultValue: "Terminal"), systemImage: "terminal.fill", shortcut: newSurfaceShortcut, action: createTerminal ) emptyPaneActionButton( - title: "Browser", + title: String(localized: "emptyPane.browser", defaultValue: "Browser"), systemImage: "globe", shortcut: openBrowserShortcut, action: createBrowser ) + + emptyPaneActionButton( + title: String(localized: "emptyPane.codeEditor", defaultValue: "Code Editor"), + systemImage: "curlybraces.square", + shortcut: nil, + action: createCodeEditor + ) } } .frame(maxWidth: .infinity, maxHeight: .infinity) diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index 52654c8d0144..5e66570126e6 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -515,6 +515,11 @@ struct cmuxApp: App { } } + Button(String(localized: "menu.file.newCodeEditorTab", defaultValue: "New Code Editor Tab")) { + _ = AppDelegate.shared?.openCodeEditor(tabManager: activeTabManager) + } + .disabled(BrowserAvailabilitySettings.isDisabled()) + splitCommandButton(title: String(localized: "menu.file.openFolder", defaultValue: "Open Folder…"), shortcut: menuShortcut(for: .openFolder)) { AppDelegate.shared?.showOpenFolderPanel() } diff --git a/cmuxTests/TabManagerSessionSnapshotTests.swift b/cmuxTests/TabManagerSessionSnapshotTests.swift index 2b2c9074fa91..bcedcbaa5a35 100644 --- a/cmuxTests/TabManagerSessionSnapshotTests.swift +++ b/cmuxTests/TabManagerSessionSnapshotTests.swift @@ -48,6 +48,36 @@ final class TabManagerSessionSnapshotTests: XCTestCase { XCTAssertNotNil(manager.selectedTabId) } + func testSessionSnapshotRestoresCodeEditorPanelTypeAndURL() { + let manager = TabManager() + guard let workspace = manager.selectedWorkspace, + let paneId = workspace.bonsplitController.focusedPaneId, + let editorURL = URL(string: "https://vscode.dev/"), + let editorPanel = workspace.newCodeEditorSurface(inPane: paneId, url: editorURL, focus: true) else { + XCTFail("Expected code editor setup to succeed") + return + } + + let snapshot = manager.sessionSnapshot(includeScrollback: false) + guard let panelSnapshot = snapshot.workspaces.first?.panels.first(where: { $0.id == editorPanel.id }) else { + XCTFail("Expected code editor panel snapshot") + return + } + XCTAssertEqual(panelSnapshot.type, .codeEditor) + XCTAssertEqual(panelSnapshot.browser?.urlString, editorURL.absoluteString) + + let restored = TabManager() + restored.restoreSessionSnapshot(snapshot) + guard let restoredWorkspace = restored.selectedWorkspace, + let restoredEditor = restoredWorkspace.panels.values.compactMap({ $0 as? BrowserPanel }).first(where: { $0.panelType == .codeEditor }) else { + XCTFail("Expected restored code editor panel") + return + } + + XCTAssertEqual(restoredEditor.surfaceRole, .codeEditor) + XCTAssertEqual(restoredEditor.currentURL?.absoluteString, editorURL.absoluteString) + } + func testRestoredPersistentSSHBrowserOnlyWorkspaceAutoConnectsWithoutForegroundAuthTerminal() { let browserPanelId = UUID() let browserOnlySnapshot = Self.persistentSSHWorkspaceSnapshot( diff --git a/cmuxTests/TabManagerUnitTests.swift b/cmuxTests/TabManagerUnitTests.swift index d2716228a0ee..0df60cd8ab3f 100644 --- a/cmuxTests/TabManagerUnitTests.swift +++ b/cmuxTests/TabManagerUnitTests.swift @@ -2152,6 +2152,36 @@ final class TabManagerSurfaceCreationTests: XCTestCase { XCTAssertEqual(workspace.focusedPanelId, browserPanelId, "Expected opened browser surface to be focused") } + func testOpenCodeEditorCreatesFirstClassEditorSurface() { + let manager = TabManager() + guard let workspace = manager.selectedWorkspace, + let paneId = workspace.bonsplitController.focusedPaneId else { + XCTFail("Expected focused workspace and pane") + return + } + + _ = workspace.newTerminalSurface(inPane: paneId, focus: false) + + guard let editorPanelId = manager.openCodeEditor(insertAtEnd: true), + let editorPanel = workspace.browserPanel(for: editorPanelId) else { + XCTFail("Expected code editor panel to be created") + return + } + + XCTAssertEqual(editorPanel.panelType, .codeEditor) + XCTAssertEqual(editorPanel.surfaceRole, .codeEditor) + XCTAssertEqual(editorPanel.currentURL?.absoluteString, "https://vscode.dev/") + XCTAssertNil(manager.focusedBrowserPanel, "Code editor surfaces should not be treated as browser shortcut targets") + + let tabs = workspace.bonsplitController.tabs(inPane: paneId) + guard let lastSurface = tabs.last else { + XCTFail("Expected at least one surface in pane") + return + } + XCTAssertEqual(workspace.panelIdFromSurfaceId(lastSurface.id), editorPanelId) + XCTAssertEqual(lastSurface.kind, Workspace.SurfaceKind.codeEditor) + } + func testOpenBrowserInWorkspaceSplitRightSelectsTargetWorkspaceAndCreatesSplit() { let manager = TabManager() guard let initialWorkspace = manager.selectedWorkspace else { From f70af7f98a63af629517c159b428e3b41d827e18 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:15:02 -0700 Subject: [PATCH 02/18] Fix code editor closed history title --- Resources/Localizable.xcstrings | 1 + Sources/ClosedItemHistory.swift | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 3abb7b88f347..1b06feada757 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -14,6 +14,7 @@ "emptyPane.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, "emptyPane.terminal": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } }, "emptyPane.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Empty Panel" } }, "ja": { "stringUnit": { "state": "translated", "value": "空のパネル" } } } }, + "menu.history.recentlyClosed.panel.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, "menu.file.newCodeEditorTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "New Code Editor Tab" } }, "ja": { "stringUnit": { "state": "translated", "value": "新しいコードエディタタブ" } } } }, "taskManager.row.surfaceType.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, "cli.hooks.error.configDirectoryIsFile": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "cmux could not create the hooks directory: a file exists at %@; remove or rename the conflicting file and re-run `cmux hooks setup`" } }, "ja": { "stringUnit": { "state": "translated", "value": "cmux は hooks ディレクトリを作成できませんでした: %@ にファイルがあります。競合するファイルを削除または名前変更してから `cmux hooks setup` を再実行してください" } } } }, diff --git a/Sources/ClosedItemHistory.swift b/Sources/ClosedItemHistory.swift index 573b3874f282..de81265706ef 100644 --- a/Sources/ClosedItemHistory.swift +++ b/Sources/ClosedItemHistory.swift @@ -387,6 +387,8 @@ final class ClosedItemHistoryStore: ObservableObject { return String(localized: "menu.history.recentlyClosed.panel.terminal", defaultValue: "Terminal") case .browser: return String(localized: "menu.history.recentlyClosed.panel.browser", defaultValue: "Browser") + case .codeEditor: + return String(localized: "menu.history.recentlyClosed.panel.codeEditor", defaultValue: "Code Editor") case .markdown: return String(localized: "menu.history.recentlyClosed.panel.markdown", defaultValue: "Markdown") case .filePreview: From 5ed8f3c975af6486a0d6dcb16059315725ef7cb1 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:29:34 -0700 Subject: [PATCH 03/18] Address code editor review feedback --- Resources/Localizable.xcstrings | 30 +++++++++++++-------------- Sources/AppDelegate.swift | 16 +++++++------- Sources/Panels/BrowserPanelView.swift | 6 ------ 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 1b06feada757..50b314526854 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -2,21 +2,21 @@ "sourceLanguage": "en", "version": "1.0", "strings": { - "codeEditor.newTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, - "command.codeEditorSplitDown.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor Layout" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタレイアウト" } } } }, - "command.codeEditorSplitDown.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Split Code Editor Down" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタを下に分割" } } } }, - "command.codeEditorSplitRight.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor Layout" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタレイアウト" } } } }, - "command.codeEditorSplitRight.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Split Code Editor Right" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタを右に分割" } } } }, - "command.newCodeEditorTab.subtitle": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Tab" } }, "ja": { "stringUnit": { "state": "translated", "value": "タブ" } } } }, - "command.newCodeEditorTab.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "New Tab (Code Editor)" } }, "ja": { "stringUnit": { "state": "translated", "value": "新規タブ(コードエディタ)" } } } }, - "commandPalette.kind.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, - "emptyPane.browser": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Browser" } }, "ja": { "stringUnit": { "state": "translated", "value": "ブラウザ" } } } }, - "emptyPane.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, - "emptyPane.terminal": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Terminal" } }, "ja": { "stringUnit": { "state": "translated", "value": "ターミナル" } } } }, - "emptyPane.title": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Empty Panel" } }, "ja": { "stringUnit": { "state": "translated", "value": "空のパネル" } } } }, - "menu.history.recentlyClosed.panel.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, - "menu.file.newCodeEditorTab": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "New Code Editor Tab" } }, "ja": { "stringUnit": { "state": "translated", "value": "新しいコードエディタタブ" } } } }, - "taskManager.row.surfaceType.codeEditor": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "Code Editor" } }, "ja": { "stringUnit": { "state": "translated", "value": "コードエディタ" } } } }, + "codeEditor.newTab": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Uređivač koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor"}},"es":{"stringUnit":{"state":"translated","value":"Editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタ"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"pl":{"stringUnit":{"state":"translated","value":"Edytor kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Редактор кода"}},"th":{"stringUnit":{"state":"translated","value":"ตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici"}},"uk":{"stringUnit":{"state":"translated","value":"Редактор коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器"}}}}, + "command.codeEditorSplitDown.subtitle": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تخطيط محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Raspored uređivača koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditorlayout"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor-Layout"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor Layout"}},"es":{"stringUnit":{"state":"translated","value":"Diseño del editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Disposition de l’éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Layout editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタレイアウト"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기 레이아웃"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditoroppsett"}},"pl":{"stringUnit":{"state":"translated","value":"Układ edytora kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Layout do editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Макет редактора кода"}},"th":{"stringUnit":{"state":"translated","value":"เค้าโครงตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici düzeni"}},"uk":{"stringUnit":{"state":"translated","value":"Макет редактора коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器布局"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器版面配置"}}}}, + "command.codeEditorSplitDown.title": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تقسيم محرر الأكواد للأسفل"}},"bs":{"stringUnit":{"state":"translated","value":"Podijeli uređivač koda dolje"}},"da":{"stringUnit":{"state":"translated","value":"Opdel kodeeditor ned"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor nach unten teilen"}},"en":{"stringUnit":{"state":"translated","value":"Split Code Editor Down"}},"es":{"stringUnit":{"state":"translated","value":"Dividir editor de código abajo"}},"fr":{"stringUnit":{"state":"translated","value":"Scinder l’éditeur de code vers le bas"}},"it":{"stringUnit":{"state":"translated","value":"Dividi editor di codice in basso"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタを下に分割"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기 아래로 분할"}},"nb":{"stringUnit":{"state":"translated","value":"Del kodeeditor ned"}},"pl":{"stringUnit":{"state":"translated","value":"Podziel edytor kodu w dół"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Dividir editor de código abaixo"}},"ru":{"stringUnit":{"state":"translated","value":"Разделить редактор кода вниз"}},"th":{"stringUnit":{"state":"translated","value":"แบ่งตัวแก้ไขโค้ดลง"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyiciyi aşağı böl"}},"uk":{"stringUnit":{"state":"translated","value":"Розділити редактор коду вниз"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"向下拆分代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"向下分割程式碼編輯器"}}}}, + "command.codeEditorSplitRight.subtitle": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تخطيط محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Raspored uređivača koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditorlayout"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor-Layout"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor Layout"}},"es":{"stringUnit":{"state":"translated","value":"Diseño del editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Disposition de l’éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Layout editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタレイアウト"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기 레이아웃"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditoroppsett"}},"pl":{"stringUnit":{"state":"translated","value":"Układ edytora kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Layout do editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Макет редактора кода"}},"th":{"stringUnit":{"state":"translated","value":"เค้าโครงตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici düzeni"}},"uk":{"stringUnit":{"state":"translated","value":"Макет редактора коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器布局"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器版面配置"}}}}, + "command.codeEditorSplitRight.title": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تقسيم محرر الأكواد إلى اليمين"}},"bs":{"stringUnit":{"state":"translated","value":"Podijeli uređivač koda desno"}},"da":{"stringUnit":{"state":"translated","value":"Opdel kodeeditor til højre"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor nach rechts teilen"}},"en":{"stringUnit":{"state":"translated","value":"Split Code Editor Right"}},"es":{"stringUnit":{"state":"translated","value":"Dividir editor de código a la derecha"}},"fr":{"stringUnit":{"state":"translated","value":"Scinder l’éditeur de code à droite"}},"it":{"stringUnit":{"state":"translated","value":"Dividi editor di codice a destra"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタを右に分割"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기 오른쪽 분할"}},"nb":{"stringUnit":{"state":"translated","value":"Del kodeeditor til høyre"}},"pl":{"stringUnit":{"state":"translated","value":"Podziel edytor kodu w prawo"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Dividir editor de código à direita"}},"ru":{"stringUnit":{"state":"translated","value":"Разделить редактор кода вправо"}},"th":{"stringUnit":{"state":"translated","value":"แบ่งตัวแก้ไขโค้ดไปทางขวา"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyiciyi sağa böl"}},"uk":{"stringUnit":{"state":"translated","value":"Розділити редактор коду праворуч"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"向右拆分代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"向右分割程式碼編輯器"}}}}, + "command.newCodeEditorTab.subtitle": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تبويب"}},"bs":{"stringUnit":{"state":"translated","value":"Kartica"}},"da":{"stringUnit":{"state":"translated","value":"Fane"}},"de":{"stringUnit":{"state":"translated","value":"Tab"}},"en":{"stringUnit":{"state":"translated","value":"Tab"}},"es":{"stringUnit":{"state":"translated","value":"Pestaña"}},"fr":{"stringUnit":{"state":"translated","value":"Onglet"}},"it":{"stringUnit":{"state":"translated","value":"Scheda"}},"ja":{"stringUnit":{"state":"translated","value":"タブ"}},"ko":{"stringUnit":{"state":"translated","value":"탭"}},"nb":{"stringUnit":{"state":"translated","value":"Fane"}},"pl":{"stringUnit":{"state":"translated","value":"Karta"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Aba"}},"ru":{"stringUnit":{"state":"translated","value":"Вкладка"}},"th":{"stringUnit":{"state":"translated","value":"แท็บ"}},"tr":{"stringUnit":{"state":"translated","value":"Sekme"}},"uk":{"stringUnit":{"state":"translated","value":"Вкладка"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"标签页"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"分頁"}}}}, + "command.newCodeEditorTab.title": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تبويب جديد (محرر الأكواد)"}},"bs":{"stringUnit":{"state":"translated","value":"Nova kartica (uređivač koda)"}},"da":{"stringUnit":{"state":"translated","value":"Ny fane (kodeeditor)"}},"de":{"stringUnit":{"state":"translated","value":"Neuer Tab (Code-Editor)"}},"en":{"stringUnit":{"state":"translated","value":"New Tab (Code Editor)"}},"es":{"stringUnit":{"state":"translated","value":"Nueva pestaña (editor de código)"}},"fr":{"stringUnit":{"state":"translated","value":"Nouvel onglet (éditeur de code)"}},"it":{"stringUnit":{"state":"translated","value":"Nuova scheda (editor di codice)"}},"ja":{"stringUnit":{"state":"translated","value":"新規タブ(コードエディタ)"}},"ko":{"stringUnit":{"state":"translated","value":"새 탭(코드 편집기)"}},"nb":{"stringUnit":{"state":"translated","value":"Ny fane (kodeeditor)"}},"pl":{"stringUnit":{"state":"translated","value":"Nowa karta (edytor kodu)"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Nova aba (editor de código)"}},"ru":{"stringUnit":{"state":"translated","value":"Новая вкладка (редактор кода)"}},"th":{"stringUnit":{"state":"translated","value":"แท็บใหม่ (ตัวแก้ไขโค้ด)"}},"tr":{"stringUnit":{"state":"translated","value":"Yeni sekme (kod düzenleyici)"}},"uk":{"stringUnit":{"state":"translated","value":"Нова вкладка (редактор коду)"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"新建标签页(代码编辑器)"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"新增分頁(程式碼編輯器)"}}}}, + "commandPalette.kind.codeEditor": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Uređivač koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor"}},"es":{"stringUnit":{"state":"translated","value":"Editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタ"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"pl":{"stringUnit":{"state":"translated","value":"Edytor kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Редактор кода"}},"th":{"stringUnit":{"state":"translated","value":"ตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici"}},"uk":{"stringUnit":{"state":"translated","value":"Редактор коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器"}}}}, + "emptyPane.browser": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"المتصفح"}},"bs":{"stringUnit":{"state":"translated","value":"Preglednik"}},"da":{"stringUnit":{"state":"translated","value":"Browser"}},"de":{"stringUnit":{"state":"translated","value":"Browser"}},"en":{"stringUnit":{"state":"translated","value":"Browser"}},"es":{"stringUnit":{"state":"translated","value":"Navegador"}},"fr":{"stringUnit":{"state":"translated","value":"Navigateur"}},"it":{"stringUnit":{"state":"translated","value":"Browser"}},"ja":{"stringUnit":{"state":"translated","value":"ブラウザ"}},"ko":{"stringUnit":{"state":"translated","value":"브라우저"}},"nb":{"stringUnit":{"state":"translated","value":"Nettleser"}},"pl":{"stringUnit":{"state":"translated","value":"Przeglądarka"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Navegador"}},"ru":{"stringUnit":{"state":"translated","value":"Браузер"}},"th":{"stringUnit":{"state":"translated","value":"เบราว์เซอร์"}},"tr":{"stringUnit":{"state":"translated","value":"Tarayıcı"}},"uk":{"stringUnit":{"state":"translated","value":"Браузер"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"浏览器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"瀏覽器"}}}}, + "emptyPane.codeEditor": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Uređivač koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor"}},"es":{"stringUnit":{"state":"translated","value":"Editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタ"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"pl":{"stringUnit":{"state":"translated","value":"Edytor kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Редактор кода"}},"th":{"stringUnit":{"state":"translated","value":"ตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici"}},"uk":{"stringUnit":{"state":"translated","value":"Редактор коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器"}}}}, + "emptyPane.terminal": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"الطرفية"}},"bs":{"stringUnit":{"state":"translated","value":"Terminal"}},"da":{"stringUnit":{"state":"translated","value":"Terminal"}},"de":{"stringUnit":{"state":"translated","value":"Terminal"}},"en":{"stringUnit":{"state":"translated","value":"Terminal"}},"es":{"stringUnit":{"state":"translated","value":"Terminal"}},"fr":{"stringUnit":{"state":"translated","value":"Terminal"}},"it":{"stringUnit":{"state":"translated","value":"Terminale"}},"ja":{"stringUnit":{"state":"translated","value":"ターミナル"}},"ko":{"stringUnit":{"state":"translated","value":"터미널"}},"nb":{"stringUnit":{"state":"translated","value":"Terminal"}},"pl":{"stringUnit":{"state":"translated","value":"Terminal"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Terminal"}},"ru":{"stringUnit":{"state":"translated","value":"Терминал"}},"th":{"stringUnit":{"state":"translated","value":"เทอร์มินัล"}},"tr":{"stringUnit":{"state":"translated","value":"Terminal"}},"uk":{"stringUnit":{"state":"translated","value":"Термінал"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"终端"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"終端機"}}}}, + "emptyPane.title": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"لوحة فارغة"}},"bs":{"stringUnit":{"state":"translated","value":"Prazan panel"}},"da":{"stringUnit":{"state":"translated","value":"Tomt panel"}},"de":{"stringUnit":{"state":"translated","value":"Leeres Panel"}},"en":{"stringUnit":{"state":"translated","value":"Empty Panel"}},"es":{"stringUnit":{"state":"translated","value":"Panel vacío"}},"fr":{"stringUnit":{"state":"translated","value":"Panneau vide"}},"it":{"stringUnit":{"state":"translated","value":"Pannello vuoto"}},"ja":{"stringUnit":{"state":"translated","value":"空のパネル"}},"ko":{"stringUnit":{"state":"translated","value":"빈 패널"}},"nb":{"stringUnit":{"state":"translated","value":"Tomt panel"}},"pl":{"stringUnit":{"state":"translated","value":"Pusty panel"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Painel vazio"}},"ru":{"stringUnit":{"state":"translated","value":"Пустая панель"}},"th":{"stringUnit":{"state":"translated","value":"แผงว่าง"}},"tr":{"stringUnit":{"state":"translated","value":"Boş panel"}},"uk":{"stringUnit":{"state":"translated","value":"Порожня панель"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"空面板"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"空面板"}}}}, + "menu.history.recentlyClosed.panel.codeEditor": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Uređivač koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor"}},"es":{"stringUnit":{"state":"translated","value":"Editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタ"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"pl":{"stringUnit":{"state":"translated","value":"Edytor kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Редактор кода"}},"th":{"stringUnit":{"state":"translated","value":"ตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici"}},"uk":{"stringUnit":{"state":"translated","value":"Редактор коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器"}}}}, + "menu.file.newCodeEditorTab": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"تبويب محرر أكواد جديد"}},"bs":{"stringUnit":{"state":"translated","value":"Nova kartica uređivača koda"}},"da":{"stringUnit":{"state":"translated","value":"Ny kodeeditorfane"}},"de":{"stringUnit":{"state":"translated","value":"Neuer Code-Editor-Tab"}},"en":{"stringUnit":{"state":"translated","value":"New Code Editor Tab"}},"es":{"stringUnit":{"state":"translated","value":"Nueva pestaña de editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Nouvel onglet d’éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Nuova scheda editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"新しいコードエディタタブ"}},"ko":{"stringUnit":{"state":"translated","value":"새 코드 편집기 탭"}},"nb":{"stringUnit":{"state":"translated","value":"Ny kodeeditorfane"}},"pl":{"stringUnit":{"state":"translated","value":"Nowa karta edytora kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Nova aba de editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Новая вкладка редактора кода"}},"th":{"stringUnit":{"state":"translated","value":"แท็บตัวแก้ไขโค้ดใหม่"}},"tr":{"stringUnit":{"state":"translated","value":"Yeni kod düzenleyici sekmesi"}},"uk":{"stringUnit":{"state":"translated","value":"Нова вкладка редактора коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"新建代码编辑器标签页"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"新增程式碼編輯器分頁"}}}}, + "taskManager.row.surfaceType.codeEditor": {"extractionState":"manual","localizations":{"ar":{"stringUnit":{"state":"translated","value":"محرر الأكواد"}},"bs":{"stringUnit":{"state":"translated","value":"Uređivač koda"}},"da":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"de":{"stringUnit":{"state":"translated","value":"Code-Editor"}},"en":{"stringUnit":{"state":"translated","value":"Code Editor"}},"es":{"stringUnit":{"state":"translated","value":"Editor de código"}},"fr":{"stringUnit":{"state":"translated","value":"Éditeur de code"}},"it":{"stringUnit":{"state":"translated","value":"Editor di codice"}},"ja":{"stringUnit":{"state":"translated","value":"コードエディタ"}},"ko":{"stringUnit":{"state":"translated","value":"코드 편집기"}},"nb":{"stringUnit":{"state":"translated","value":"Kodeeditor"}},"pl":{"stringUnit":{"state":"translated","value":"Edytor kodu"}},"pt-BR":{"stringUnit":{"state":"translated","value":"Editor de código"}},"ru":{"stringUnit":{"state":"translated","value":"Редактор кода"}},"th":{"stringUnit":{"state":"translated","value":"ตัวแก้ไขโค้ด"}},"tr":{"stringUnit":{"state":"translated","value":"Kod düzenleyici"}},"uk":{"stringUnit":{"state":"translated","value":"Редактор коду"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"代码编辑器"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"程式碼編輯器"}}}}, "cli.hooks.error.configDirectoryIsFile": { "extractionState": "manual", "localizations": { "en": { "stringUnit": { "state": "translated", "value": "cmux could not create the hooks directory: a file exists at %@; remove or rename the conflicting file and re-run `cmux hooks setup`" } }, "ja": { "stringUnit": { "state": "translated", "value": "cmux は hooks ディレクトリを作成できませんでした: %@ にファイルがあります。競合するファイルを削除または名前変更してから `cmux hooks setup` を再実行してください" } } } }, "cli.hooks.antigravity.aborted": { "extractionState": "manual", "localizations": {"ar":{"stringUnit":{"state":"translated","value":"Aborted."}},"bs":{"stringUnit":{"state":"translated","value":"Aborted."}},"da":{"stringUnit":{"state":"translated","value":"Aborted."}},"de":{"stringUnit":{"state":"translated","value":"Aborted."}},"en":{"stringUnit":{"state":"translated","value":"Aborted."}},"es":{"stringUnit":{"state":"translated","value":"Aborted."}},"fr":{"stringUnit":{"state":"translated","value":"Aborted."}},"it":{"stringUnit":{"state":"translated","value":"Aborted."}},"ja":{"stringUnit":{"state":"translated","value":"中止しました。"}},"ko":{"stringUnit":{"state":"translated","value":"Aborted."}},"nb":{"stringUnit":{"state":"translated","value":"Aborted."}},"pl":{"stringUnit":{"state":"translated","value":"Aborted."}},"pt-BR":{"stringUnit":{"state":"translated","value":"Aborted."}},"ru":{"stringUnit":{"state":"translated","value":"Aborted."}},"th":{"stringUnit":{"state":"translated","value":"Aborted."}},"tr":{"stringUnit":{"state":"translated","value":"Aborted."}},"uk":{"stringUnit":{"state":"translated","value":"Aborted."}},"zh-Hans":{"stringUnit":{"state":"translated","value":"Aborted."}},"zh-Hant":{"stringUnit":{"state":"translated","value":"Aborted."}}} }, "cli.hooks.antigravity.alreadyUpToDate": { "extractionState": "manual", "localizations": {"ar":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"bs":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"da":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"de":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"en":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"es":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"fr":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"it":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"ja":{"stringUnit":{"state":"translated","value":"%@ hooks はすでに %@ で最新です"}},"ko":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"nb":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"pl":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"pt-BR":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"ru":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"th":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"tr":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"uk":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"zh-Hans":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}},"zh-Hant":{"stringUnit":{"state":"translated","value":"%@ hooks already up to date at %@"}}} }, diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 13739860171a..2501d9238236 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6860,9 +6860,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return false } - let targetWorkspaceId = targetTabManager.selectedWorkspace?.id - ?? targetTabManager.tabs.first?.id - ?? targetTabManager.addWorkspace(select: true).id + let targetWorkspace = targetTabManager.selectedWorkspace + ?? targetTabManager.tabs.first + ?? targetTabManager.addWorkspace(select: true) + let targetWorkspaceId = targetWorkspace.id let normalizedDirectoryURL = directoryURL.standardizedFileURL VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in @@ -6902,9 +6903,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } - let targetWorkspaceId = targetTabManager.selectedWorkspace?.id - ?? targetTabManager.tabs.first?.id - ?? targetTabManager.addWorkspace(select: true).id + let targetWorkspace = targetTabManager.selectedWorkspace + ?? targetTabManager.tabs.first + ?? targetTabManager.addWorkspace(select: true) + let targetWorkspaceId = targetWorkspace.id if let requestedURL { return targetTabManager.openCodeEditor( @@ -6924,7 +6926,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } - let directoryPath = targetTabManager.selectedWorkspace?.currentDirectory + let directoryPath = targetWorkspace.currentDirectory .trimmingCharacters(in: .whitespacesAndNewlines) guard let directoryPath, !directoryPath.isEmpty, diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index d95414d5da3b..78a3ef6b0bf8 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -768,12 +768,6 @@ struct BrowserPanelView: View { .onPreferenceChange(BrowserAddressBarHeightPreferenceKey.self) { height in addressBarHeight = height } - .onChange(of: panel.surfaceRole.showsBrowserChrome) { _, showsBrowserChrome in - if !showsBrowserChrome { - addressBarHeight = 0 - omnibarPillFrame = .zero - } - } .onReceive(NotificationCenter.default.publisher(for: .webViewDidReceiveClick).filter { [weak panel] note in // Only handle clicks from our own webview. guard let webView = note.object as? CmuxWebView else { return false } From 254ed0dfef2987c1ca600b8e3d881c1e8dfcd537 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:34:48 -0700 Subject: [PATCH 04/18] Fix code editor directory fallback compile --- Sources/AppDelegate.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 2501d9238236..d7139233fb17 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6928,8 +6928,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let directoryPath = targetWorkspace.currentDirectory .trimmingCharacters(in: .whitespacesAndNewlines) - guard let directoryPath, - !directoryPath.isEmpty, + guard !directoryPath.isEmpty, let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL() else { return openDefaultCodeEditor() } From 2447b2604fbc264988003a218bd2a3afcc77d13e Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 26 May 2026 01:45:38 -0700 Subject: [PATCH 05/18] Return code editor panel id during async open --- Sources/AppDelegate.swift | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index d7139233fb17..b614fd5d7647 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -6933,30 +6933,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return openDefaultCodeEditor() } + guard let panelId = openDefaultCodeEditor() else { + NSSound.beep() + return nil + } + VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, directoryPath: directoryPath ) else { - if openDefaultCodeEditor() == nil { - NSSound.beep() - } return } - guard targetTabManager.openCodeEditor( - inWorkspace: targetWorkspaceId, - url: openFolderURL, - preferSplitRight: preferSplitRight, - insertAtEnd: insertAtEnd - ) != nil else { - NSSound.beep() - return - } + targetTabManager.browserPanel(tabId: targetWorkspaceId, panelId: panelId)? + .navigate(to: openFolderURL) } - return nil + return panelId } func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) { From 63f4b4631b59ac4fa5ec9404b8a2a6051ad114fb Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 02:34:43 -0700 Subject: [PATCH 06/18] Fix code editor cwd resolution --- Sources/AppDelegate.swift | 10 +++++--- cmuxTests/WorkspaceUnitTests.swift | 39 ++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 62e0ee463483..3bd92f49ee20 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7362,9 +7362,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ) } - let directoryPath = targetWorkspace.currentDirectory - .trimmingCharacters(in: .whitespacesAndNewlines) - guard !directoryPath.isEmpty, + guard let directoryPath = Self.initialCodeEditorDirectoryPath(for: targetWorkspace), let vscodeApplicationURL = TerminalDirectoryOpenTarget.vscodeInline.applicationURL() else { return openDefaultCodeEditor() } @@ -7390,6 +7388,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return panelId } + static func initialCodeEditorDirectoryPath(for workspace: Workspace) -> String? { + workspace.resolvedWorkingDirectory() + } + func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) { guard TerminalDirectoryOpenTarget.vscodeInline.isAvailable() else { NSSound.beep() @@ -15026,7 +15028,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent onExecuted?() return true case .newCodeEditor: - guard context.tabManager.openCodeEditor(insertAtEnd: true) != nil else { + guard openCodeEditor(tabManager: context.tabManager, insertAtEnd: true) != nil else { return false } onExecuted?() diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 97a810ffd7f3..3e678d600873 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -4427,6 +4427,45 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { ) } + func testInitialCodeEditorDirectoryUsesFocusedPanelDirectoryWhenWorkspaceDirectoryIsEmpty() { + let workspace = Workspace() + guard let focusedPanelId = workspace.focusedPanelId else { + XCTFail("Expected focused panel in new workspace") + return + } + + let focusedDirectory = "/tmp/cmux-focused-editor-cwd" + workspace.currentDirectory = "" + workspace.panelDirectories[focusedPanelId] = focusedDirectory + + XCTAssertEqual(workspace.resolvedWorkingDirectory(), focusedDirectory) + XCTAssertEqual(AppDelegate.initialCodeEditorDirectoryPath(for: workspace), focusedDirectory) + } + + func testInitialCodeEditorDirectoryUsesRequestedTerminalDirectoryBeforeLiveReport() { + let workspace = Workspace() + guard let paneId = workspace.bonsplitController.focusedPaneId else { + XCTFail("Expected focused pane in new workspace") + return + } + + let requestedDirectory = "/tmp/cmux-requested-editor-cwd" + guard let requestedPanel = workspace.newTerminalSurface( + inPane: paneId, + focus: true, + workingDirectory: requestedDirectory + ) else { + XCTFail("Expected terminal panel with requested cwd") + return + } + + workspace.currentDirectory = "" + workspace.panelDirectories.removeValue(forKey: requestedPanel.id) + + XCTAssertEqual(workspace.resolvedWorkingDirectory(), requestedDirectory) + XCTAssertEqual(AppDelegate.initialCodeEditorDirectoryPath(for: workspace), requestedDirectory) + } + func testNewTerminalSplitSkipsFreedInheritedSurfacePointer() throws { #if DEBUG let workspace = Workspace() From ba42617dc79c563d353fc8c95d81689aa5e0479b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 02:43:05 -0700 Subject: [PATCH 07/18] Fix editor surface routing and metadata --- Sources/TerminalController.swift | 4 +- Sources/Workspace.swift | 20 +++++- Sources/WorkspaceContentView.swift | 3 +- .../AppDelegateIssue2907RoutingTests.swift | 68 +++++++++++++++++++ 4 files changed, 88 insertions(+), 7 deletions(-) diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index ed28be9d1693..244282cf3552 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -3300,7 +3300,7 @@ class TerminalController { "webviews": [] ] - if panel.panelType == .browser, let browserPanel = panel as? BrowserPanel { + if let browserPanel = panel as? BrowserPanel { let webContentPID = CmuxWebContentProcessIdentifier.pid(for: browserPanel.webView) let url = browserPanel.currentURL?.absoluteString ?? "" let webViewLifecycle = browserPanel.webViewLifecycleTopPayload() @@ -3480,7 +3480,7 @@ class TerminalController { "tty": v2OrNull(workspace.surfaceTTYNames[panel.id]) ] - if panel.panelType == .browser, let browserPanel = panel as? BrowserPanel { + if let browserPanel = panel as? BrowserPanel { item["url"] = browserPanel.currentURL?.absoluteString ?? "" } else { item["url"] = NSNull() diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 2826520e0d96..a39f2b78048e 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -15131,6 +15131,21 @@ final class Workspace: Identifiable, ObservableObject { ) } + @discardableResult + func openCodeEditorFromPane(_ pane: PaneID) -> UUID? { + if let owningTabManager, owningTabManager.selectedTabId != id { + owningTabManager.selectWorkspace(self) + } + bonsplitController.focusPane(pane) + if let selectedTab = bonsplitController.selectedTab(inPane: pane) { + applyTabSelection(tabId: selectedTab.id, inPane: pane) + } + if let panelId = AppDelegate.shared?.openCodeEditor(tabManager: owningTabManager) { + return panelId + } + return newCodeEditorSurface(inPane: pane, focus: true)?.id + } + /// Creates a sidebar extension browser tab in the requested pane and returns its panel. /// /// - Parameters: @@ -19425,8 +19440,7 @@ extension Workspace: BonsplitDelegate { debugSource: "surfaceTabBar.cloudVM" ) case .newCodeEditor: - bonsplitController.focusPane(pane) - _ = newCodeEditorSurface(inPane: pane, focus: true) + _ = openCodeEditorFromPane(pane) case .newTerminal, .newBrowser, .splitRight, .splitDown: break } @@ -19506,7 +19520,7 @@ extension Workspace: BonsplitDelegate { case "browser": _ = newBrowserSurface(inPane: pane) case SurfaceKind.codeEditor, "codeEditor", "code_editor": - _ = newCodeEditorSurface(inPane: pane) + _ = openCodeEditorFromPane(pane) default: _ = newTerminalSurface(inPane: pane) } diff --git a/Sources/WorkspaceContentView.swift b/Sources/WorkspaceContentView.swift index 74010e373f94..362161237013 100644 --- a/Sources/WorkspaceContentView.swift +++ b/Sources/WorkspaceContentView.swift @@ -797,8 +797,7 @@ struct EmptyPanelView: View { #if DEBUG cmuxDebugLog("emptyPane.newCodeEditor pane=\(paneId.id.uuidString.prefix(5))") #endif - focusPane() - _ = workspace.newCodeEditorSurface(inPane: paneId) + _ = workspace.openCodeEditorFromPane(paneId) } private var newSurfaceShortcut: StoredShortcut { diff --git a/cmuxTests/AppDelegateIssue2907RoutingTests.swift b/cmuxTests/AppDelegateIssue2907RoutingTests.swift index af4217525216..fd3df28eabfc 100644 --- a/cmuxTests/AppDelegateIssue2907RoutingTests.swift +++ b/cmuxTests/AppDelegateIssue2907RoutingTests.swift @@ -93,6 +93,74 @@ final class AppDelegateIssue2907RoutingTests: XCTestCase { ) } + func testSystemTreeSerializesEditorSurfaceURLAndWebViewMetadata() throws { + _ = NSApplication.shared + let previousAppDelegate = AppDelegate.shared + let previousBrowserDisabled = UserDefaults.standard.object(forKey: BrowserAvailabilitySettings.disabledKey) + let app = AppDelegate() + defer { + if let previousBrowserDisabled { + UserDefaults.standard.set(previousBrowserDisabled, forKey: BrowserAvailabilitySettings.disabledKey) + } else { + UserDefaults.standard.removeObject(forKey: BrowserAvailabilitySettings.disabledKey) + } + TerminalController.shared.setActiveTabManager(nil) + AppDelegate.shared = previousAppDelegate + } + BrowserAvailabilitySettings.setDisabled(false) + + let windowId = UUID() + let window = makeMainWindow(id: windowId) + defer { + app.unregisterMainWindowContextForTesting(windowId: windowId) + window.orderOut(nil) + } + + let manager = TabManager(autoWelcomeIfNeeded: false) + app.registerMainWindow( + window, + windowId: windowId, + tabManager: manager, + sidebarState: SidebarState(), + sidebarSelectionState: SidebarSelectionState(), + fileExplorerState: FileExplorerState() + ) + TerminalController.shared.setActiveTabManager(manager) + + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let editorURL = try XCTUnwrap(URL(string: "https://vscode.dev/")) + let editorPanel = try XCTUnwrap(workspace.newCodeEditorSurface( + inPane: paneId, + url: editorURL, + focus: true + )) + + let tree = try v2Result(method: "system.tree") + let windows = try XCTUnwrap(tree["windows"] as? [[String: Any]]) + var editorSurface: [String: Any]? + for windowPayload in windows { + let workspaces = windowPayload["workspaces"] as? [[String: Any]] ?? [] + for workspacePayload in workspaces { + let panes = workspacePayload["panes"] as? [[String: Any]] ?? [] + for panePayload in panes { + let surfaces = panePayload["surfaces"] as? [[String: Any]] ?? [] + if let surface = surfaces.first(where: { ($0["id"] as? String) == editorPanel.id.uuidString }) { + editorSurface = surface + } + } + } + } + + let surface = try XCTUnwrap(editorSurface) + XCTAssertEqual(surface["type"] as? String, "editor") + XCTAssertEqual(surface["url"] as? String, editorURL.absoluteString) + let webviews = try XCTUnwrap(surface["webviews"] as? [[String: Any]]) + let webview = try XCTUnwrap(webviews.first) + XCTAssertEqual(webview["surface_id"] as? String, editorPanel.id.uuidString) + XCTAssertEqual(webview["url"] as? String, editorURL.absoluteString) + } + func testWorkspaceReorderManyRoutesByWorkspaceOwnerWhenWindowIsOmitted() throws { let previousAppDelegate = AppDelegate.shared let app = AppDelegate() From c3bce0b580c695368326bead3aebd602452a6140 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 02:50:27 -0700 Subject: [PATCH 08/18] Fix sidebar editor surface kind --- .../SampleSidebarExtension/Model/SidebarInsightModel.swift | 2 ++ .../TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift | 2 ++ .../Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift | 1 + Sources/ContentView.swift | 2 ++ 4 files changed, 7 insertions(+) diff --git a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift index 3ccd6d276cd3..b34bff32ab49 100644 --- a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift +++ b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift @@ -104,6 +104,8 @@ struct SurfaceInsight: Identifiable { return "terminal" case .browser: return "globe" + case .codeEditor: + return "chevron.left.forwardslash.chevron.right" case .markdown: return "doc.text" case .filePreview: diff --git a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift index 42bb6c8d149c..d44e74948935 100644 --- a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift +++ b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift @@ -165,6 +165,8 @@ struct TabsVisibleSidebarView: View { return "terminal" case .browser: return "globe" + case .codeEditor: + return "chevron.left.forwardslash.chevron.right" case .markdown: return "doc.text" case .filePreview: diff --git a/Packages/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift b/Packages/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift index 218839b39bba..61c9d965438c 100644 --- a/Packages/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift +++ b/Packages/CmuxExtensionKit/Sources/CmuxExtensionKit/Sidebar/CMUXSidebarSurface.swift @@ -3,6 +3,7 @@ import Foundation public enum CmuxSidebarSurfaceKind: String, Codable, CaseIterable, Equatable, Sendable { case terminal case browser + case codeEditor case markdown case filePreview case rightSidebarTool diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 0d11421d467e..ec399c3a82db 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -11557,6 +11557,8 @@ struct VerticalTabsSidebar: View { return .terminal case .browser: return .browser + case .codeEditor: + return .codeEditor case .markdown: return .markdown case .filePreview: From 3821ea064bd0b0ba2c7013788992184fa0887e28 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 02:56:10 -0700 Subject: [PATCH 09/18] Handle code editor serve-web failures --- Sources/AppDelegate.swift | 43 ++++++++++++++++++++++++++++++ cmuxTests/WorkspaceUnitTests.swift | 36 +++++++++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 3bd92f49ee20..f7c45e2b854b 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7378,6 +7378,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent baseWebUIURL: serveWebURL, directoryPath: directoryPath ) else { + if Self.discardProvisionalDefaultCodeEditor( + tabManager: targetTabManager, + workspaceId: targetWorkspaceId, + panelId: panelId + ) { + NSSound.beep() + } return } @@ -7392,6 +7399,42 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent workspace.resolvedWorkingDirectory() } + @discardableResult + static func discardProvisionalDefaultCodeEditor( + tabManager: TabManager, + workspaceId: UUID, + panelId: UUID + ) -> Bool { + guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { + return false + } + return discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId) + } + + @discardableResult + static func discardProvisionalDefaultCodeEditor(in workspace: Workspace, panelId: UUID) -> Bool { + guard let panel = workspace.browserPanel(for: panelId), + shouldDiscardProvisionalDefaultCodeEditor(panel) else { + return false + } + return workspace.closePanel(panelId, force: true) + } + + static func shouldDiscardProvisionalDefaultCodeEditor(_ panel: BrowserPanel) -> Bool { + guard panel.surfaceRole == .codeEditor else { + return false + } + return isCodeEditorDefaultInitialURL(panel.currentURL) + } + + static func isCodeEditorDefaultInitialURL(_ url: URL?) -> Bool { + guard let url, + let defaultURL = BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL else { + return false + } + return url.absoluteString == defaultURL.absoluteString + } + func showOpenFolderInInlineVSCodePanel(tabManager preferredTabManager: TabManager? = nil) { guard TerminalDirectoryOpenTarget.vscodeInline.isAvailable() else { NSSound.beep() diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 3e678d600873..0be4f5fdbd7c 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -4466,6 +4466,42 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { XCTAssertEqual(AppDelegate.initialCodeEditorDirectoryPath(for: workspace), requestedDirectory) } + func testDiscardProvisionalDefaultCodeEditorClosesUntouchedFallbackPanel() throws { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let panel = try XCTUnwrap( + workspace.newCodeEditorSurface( + inPane: paneId, + focus: true, + creationPolicy: .restoration + ) + ) + let panelId = panel.id + + XCTAssertTrue(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertTrue(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) + XCTAssertNil(workspace.panels[panelId]) + } + + func testDiscardProvisionalDefaultCodeEditorPreservesNavigatedPanel() throws { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let folderURL = try XCTUnwrap(URL(string: "http://127.0.0.1:5555/?tkn=test&folder=/tmp/cmux")) + let panel = try XCTUnwrap( + workspace.newCodeEditorSurface( + inPane: paneId, + url: folderURL, + focus: true, + creationPolicy: .restoration + ) + ) + let panelId = panel.id + + XCTAssertFalse(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertFalse(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) + XCTAssertNotNil(workspace.panels[panelId]) + } + func testNewTerminalSplitSkipsFreedInheritedSurfacePointer() throws { #if DEBUG let workspace = Workspace() From 0f59d905bdba87b79c4637abb70ca6dd99f80b36 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 03:06:21 -0700 Subject: [PATCH 10/18] Fix provisional editor discard test setup --- cmuxTests/WorkspaceUnitTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 0be4f5fdbd7c..7afd3e8c9b41 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -4472,6 +4472,7 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { let panel = try XCTUnwrap( workspace.newCodeEditorSurface( inPane: paneId, + url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, focus: true, creationPolicy: .restoration ) From e2304d0c7b303214fad2af4b3e12c517e7854632 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 03:11:21 -0700 Subject: [PATCH 11/18] Route code editor splits through folder-aware open path --- Sources/AppDelegate.swift | 22 +++++++++------ Sources/ContentView.swift | 10 +++++-- cmuxTests/TabManagerUnitTests.swift | 43 +++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index f7c45e2b854b..a4c72e1f6182 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7330,6 +7330,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent tabManager preferredTabManager: TabManager? = nil, url requestedURL: URL? = nil, preferSplitRight: Bool = false, + splitDirection: SplitDirection? = nil, insertAtEnd: Bool = false ) -> UUID? { let targetTabManager = preferredTabManager @@ -7344,22 +7345,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ?? targetTabManager.addWorkspace(select: true) let targetWorkspaceId = targetWorkspace.id - if let requestedURL { + func openCodeEditorPanel(url: URL?) -> UUID? { + if let splitDirection { + if targetTabManager.selectedTabId != targetWorkspaceId { + targetTabManager.selectWorkspace(targetWorkspace) + } + return targetTabManager.createCodeEditorSplit(direction: splitDirection, url: url) + } return targetTabManager.openCodeEditor( inWorkspace: targetWorkspaceId, - url: requestedURL, + url: url, preferSplitRight: preferSplitRight, insertAtEnd: insertAtEnd ) } + if let requestedURL { + return openCodeEditorPanel(url: requestedURL) + } + func openDefaultCodeEditor() -> UUID? { - targetTabManager.openCodeEditor( - inWorkspace: targetWorkspaceId, - url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL, - preferSplitRight: preferSplitRight, - insertAtEnd: insertAtEnd - ) + openCodeEditorPanel(url: BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL) } guard let directoryPath = Self.initialCodeEditorDirectoryPath(for: targetWorkspace), diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index ec399c3a82db..82573bc9293e 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -8371,10 +8371,16 @@ struct ContentView: View { _ = tabManager.createBrowserSplit(direction: .down) } registry.register(commandId: "palette.codeEditorSplitRight") { - _ = tabManager.createCodeEditorSplit(direction: .right) + guard AppDelegate.shared?.openCodeEditor(tabManager: tabManager, splitDirection: .right) != nil else { + NSSound.beep() + return + } } registry.register(commandId: "palette.codeEditorSplitDown") { - _ = tabManager.createCodeEditorSplit(direction: .down) + guard AppDelegate.shared?.openCodeEditor(tabManager: tabManager, splitDirection: .down) != nil else { + NSSound.beep() + return + } } registry.register(commandId: "palette.browserDuplicateRight") { let url = tabManager.focusedBrowserPanel?.preferredURLStringForOmnibar().flatMap(URL.init(string:)) diff --git a/cmuxTests/TabManagerUnitTests.swift b/cmuxTests/TabManagerUnitTests.swift index e73639bd2050..5f2c0289b2a0 100644 --- a/cmuxTests/TabManagerUnitTests.swift +++ b/cmuxTests/TabManagerUnitTests.swift @@ -2407,6 +2407,49 @@ final class TabManagerSurfaceCreationTests: XCTestCase { XCTAssertEqual(lastSurface.kind, Workspace.SurfaceKind.codeEditor) } + func testOpenCodeEditorWithSplitDirectionCreatesDirectedEditorSplit() throws { + let appDelegate = AppDelegate() + let manager = TabManager() + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let sourcePaneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let sourcePanel = try XCTUnwrap( + workspace.newBrowserSurface(inPane: sourcePaneId, focus: true) + ) + workspace.currentDirectory = "" + workspace.panelDirectories.removeAll() + let initialPaneCount = workspace.bonsplitController.allPaneIds.count + + let editorPanelId = try XCTUnwrap( + appDelegate.openCodeEditor(tabManager: manager, splitDirection: .down) + ) + let editorPanel = try XCTUnwrap(workspace.browserPanel(for: editorPanelId)) + + XCTAssertEqual(editorPanel.panelType, .codeEditor) + XCTAssertEqual(editorPanel.surfaceRole, .codeEditor) + XCTAssertEqual( + editorPanel.currentURL?.absoluteString, + BrowserPanel.SurfaceRole.codeEditor.defaultInitialURL?.absoluteString + ) + XCTAssertEqual(workspace.bonsplitController.allPaneIds.count, initialPaneCount + 1) + XCTAssertEqual(workspace.focusedPanelId, editorPanelId) + + guard case .split(let root) = workspace.bonsplitController.treeSnapshot() else { + XCTFail("Expected code editor open with split direction to split the workspace root") + return + } + XCTAssertEqual(root.orientation, "vertical") + + let expectedSourcePaneId = try XCTUnwrap(workspace.paneId(forPanelId: sourcePanel.id)).id.uuidString + let expectedEditorPaneId = try XCTUnwrap(workspace.paneId(forPanelId: editorPanelId)).id.uuidString + guard case .pane(let firstPane) = root.first, + case .pane(let secondPane) = root.second else { + XCTFail("Expected split children to be panes") + return + } + XCTAssertEqual(firstPane.id, expectedSourcePaneId) + XCTAssertEqual(secondPane.id, expectedEditorPaneId) + } + func testToggleOmnibarFocusedBrowserIsSurfaceSpecific() { let manager = TabManager() guard let workspace = manager.selectedWorkspace, From 74e319212944929051dbe8ecce05522ae030d561 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 03:13:58 -0700 Subject: [PATCH 12/18] Guard async code editor folder navigation --- Sources/AppDelegate.swift | 28 +++++++++++++++++++++++++--- cmuxTests/WorkspaceUnitTests.swift | 2 ++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index a4c72e1f6182..aa7c975cff12 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7394,7 +7394,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } - targetTabManager.browserPanel(tabId: targetWorkspaceId, panelId: panelId)? + Self.provisionalDefaultCodeEditor( + tabManager: targetTabManager, + workspaceId: targetWorkspaceId, + panelId: panelId + )? .navigate(to: openFolderURL) } @@ -7419,13 +7423,31 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent @discardableResult static func discardProvisionalDefaultCodeEditor(in workspace: Workspace, panelId: UUID) -> Bool { - guard let panel = workspace.browserPanel(for: panelId), - shouldDiscardProvisionalDefaultCodeEditor(panel) else { + guard provisionalDefaultCodeEditor(in: workspace, panelId: panelId) != nil else { return false } return workspace.closePanel(panelId, force: true) } + static func provisionalDefaultCodeEditor( + tabManager: TabManager, + workspaceId: UUID, + panelId: UUID + ) -> BrowserPanel? { + guard let workspace = tabManager.tabs.first(where: { $0.id == workspaceId }) else { + return nil + } + return provisionalDefaultCodeEditor(in: workspace, panelId: panelId) + } + + static func provisionalDefaultCodeEditor(in workspace: Workspace, panelId: UUID) -> BrowserPanel? { + guard let panel = workspace.browserPanel(for: panelId), + shouldDiscardProvisionalDefaultCodeEditor(panel) else { + return nil + } + return panel + } + static func shouldDiscardProvisionalDefaultCodeEditor(_ panel: BrowserPanel) -> Bool { guard panel.surfaceRole == .codeEditor else { return false diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 7afd3e8c9b41..f01e8d7781d3 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -4480,6 +4480,7 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { let panelId = panel.id XCTAssertTrue(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertEqual(AppDelegate.provisionalDefaultCodeEditor(in: workspace, panelId: panelId)?.id, panelId) XCTAssertTrue(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) XCTAssertNil(workspace.panels[panelId]) } @@ -4499,6 +4500,7 @@ final class WorkspaceSplitWorkingDirectoryTests: XCTestCase { let panelId = panel.id XCTAssertFalse(AppDelegate.shouldDiscardProvisionalDefaultCodeEditor(panel)) + XCTAssertNil(AppDelegate.provisionalDefaultCodeEditor(in: workspace, panelId: panelId)) XCTAssertFalse(AppDelegate.discardProvisionalDefaultCodeEditor(in: workspace, panelId: panelId)) XCTAssertNotNil(workspace.panels[panelId]) } From 6260a00ee84d4954bc169e8b924362413a486261 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 03:22:54 -0700 Subject: [PATCH 13/18] Align code editor labels and icons --- .../SampleSidebarExtension/Model/SidebarInsightModel.swift | 2 +- .../TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift | 2 +- Sources/CmuxConfig.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift index b34bff32ab49..1562a17915fd 100644 --- a/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift +++ b/Examples/SampleSidebarExtensionApp/SampleSidebarExtension/Model/SidebarInsightModel.swift @@ -105,7 +105,7 @@ struct SurfaceInsight: Identifiable { case .browser: return "globe" case .codeEditor: - return "chevron.left.forwardslash.chevron.right" + return "curlybraces.square" case .markdown: return "doc.text" case .filePreview: diff --git a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift index d44e74948935..7043443f8bdc 100644 --- a/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift +++ b/Examples/TabsVisibleSidebar/TabsVisibleSidebarExtension/TabsVisibleSidebarView.swift @@ -166,7 +166,7 @@ struct TabsVisibleSidebarView: View { case .browser: return "globe" case .codeEditor: - return "chevron.left.forwardslash.chevron.right" + return "curlybraces.square" case .markdown: return "doc.text" case .filePreview: diff --git a/Sources/CmuxConfig.swift b/Sources/CmuxConfig.swift index b2904a318847..fd9927d2864e 100644 --- a/Sources/CmuxConfig.swift +++ b/Sources/CmuxConfig.swift @@ -1565,7 +1565,7 @@ struct CmuxResolvedConfigAction: Identifiable, Sendable, Hashable { title = String(localized: "command.newBrowserTab.title", defaultValue: "New Browser Tab") keywords = ["new", "browser", "tab", "surface"] case .newCodeEditor: - title = String(localized: "command.newCodeEditorTab.title", defaultValue: "New Code Editor Tab") + title = String(localized: "command.newCodeEditorTab.title", defaultValue: "New Tab (Code Editor)") keywords = ["new", "code", "editor", "tab", "surface", "vscode"] case .splitRight: title = String(localized: "command.terminalSplitRight.title", defaultValue: "Split Right") From 09e78893e0571464785285455c520e13b0d23bb9 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sat, 6 Jun 2026 03:42:24 -0700 Subject: [PATCH 14/18] Honor code editor surface defaults --- Resources/Localizable.xcstrings | 72 +++++++++++++++++++ .../App/TerminalDirectoryOpenSupport.swift | 8 +++ Sources/AppDelegate.swift | 10 ++- Sources/Panels/BrowserPanel.swift | 11 ++- Sources/Workspace.swift | 2 + cmuxTests/BrowserPanelTests.swift | 12 ++++ 6 files changed, 110 insertions(+), 5 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 31d1ecd13b83..bacc5177956c 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -175794,6 +175794,12 @@ "value": "コードエディタ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -175913,6 +175919,12 @@ "value": "コードエディタレイアウト" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្លង់កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176032,6 +176044,12 @@ "value": "コードエディタを下に分割" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បំបែកកម្មវិធីកែសម្រួលកូដចុះក្រោម" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176151,6 +176169,12 @@ "value": "コードエディタレイアウト" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ប្លង់កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176270,6 +176294,12 @@ "value": "コードエディタを右に分割" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "បំបែកកម្មវិធីកែសម្រួលកូដទៅស្តាំ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176389,6 +176419,12 @@ "value": "タブ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំង" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176508,6 +176544,12 @@ "value": "新規タブ(コードエディタ)" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំងថ្មី (កម្មវិធីកែសម្រួលកូដ)" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176627,6 +176669,12 @@ "value": "コードエディタ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -176865,6 +176913,12 @@ "value": "コードエディタ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -177222,6 +177276,12 @@ "value": "コードエディタ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -177341,6 +177401,12 @@ "value": "新しいコードエディタタブ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "ផ្ទាំងកម្មវិធីកែសម្រួលកូដថ្មី" + } + }, "ko": { "stringUnit": { "state": "translated", @@ -177460,6 +177526,12 @@ "value": "コードエディタ" } }, + "km": { + "stringUnit": { + "state": "translated", + "value": "កម្មវិធីកែសម្រួលកូដ" + } + }, "ko": { "stringUnit": { "state": "translated", diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 1d2305b371e2..4ec054bf8554 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -495,6 +495,14 @@ final class VSCodeServeWebController { } } + func ensureServeWebURL(vscodeApplicationURL: URL) async -> URL? { + await withCheckedContinuation { continuation in + ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + continuation.resume(returning: serveWebURL) + } + } + } + func stop() { let (processes, tokenFileURLs, completions): ([Process], [URL], [(URL?) -> Void]) = queue.sync { self.lifecycleGeneration &+= 1 diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index aa7c975cff12..0f231dbbc679 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7302,7 +7302,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let targetWorkspaceId = targetWorkspace.id let normalizedDirectoryURL = directoryURL.standardizedFileURL - VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + Task { @MainActor in + let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( + vscodeApplicationURL: vscodeApplicationURL + ) guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, @@ -7378,7 +7381,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } - VSCodeServeWebController.shared.ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in + Task { @MainActor in + let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( + vscodeApplicationURL: vscodeApplicationURL + ) guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 20c8c210ecf0..aa507b259a26 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -4456,7 +4456,8 @@ final class BrowserPanel: Panel, ObservableObject { renderInitialNavigation: Bool = true, preloadInitialNavigationInBackground: Bool = false, bypassInsecureHTTPHostOnce: String? = nil, - omnibarVisible: Bool = true, + useSurfaceRoleDefaultInitialURL: Bool = true, + omnibarVisible: Bool? = nil, transparentBackground: Bool = false, proxyEndpoint: BrowserProxyEndpoint? = nil, bypassRemoteProxy: Bool = false, @@ -4466,6 +4467,10 @@ final class BrowserPanel: Panel, ObservableObject { // Register fallback defaults and normalize legacy/out-of-range settings once // per process, before any setting is read below or by the SwiftUI view. Self.bootstrapBrowserDefaultsIfNeeded() + let resolvedInitialURL = initialURL + ?? (useSurfaceRoleDefaultInitialURL ? surfaceRole.defaultInitialURL : nil) + let resolvedOmnibarVisible = surfaceRole.showsBrowserChrome + && (omnibarVisible ?? surfaceRole.showsBrowserChrome) self.id = UUID() self.surfaceRole = surfaceRole self.panelType = surfaceRole.panelType @@ -4482,7 +4487,7 @@ final class BrowserPanel: Panel, ObservableObject { self.usesRemoteWorkspaceProxy = isRemoteWorkspace && !bypassRemoteProxy self.browserThemeMode = BrowserThemeSettings.mode() self.shouldPreloadInitialNavigationInBackground = preloadInitialNavigationInBackground - self.isOmnibarVisible = omnibarVisible + self.isOmnibarVisible = resolvedOmnibarVisible self.usesTransparentBackground = transparentBackground self.websiteDataStore = isRemoteWorkspace ? WKWebsiteDataStore(forIdentifier: remoteWebsiteDataStoreIdentifier ?? workspaceId) @@ -4635,7 +4640,7 @@ final class BrowserPanel: Panel, ObservableObject { recordTypedNavigation: false ) } - } else if let url = initialURL { + } else if let url = resolvedInitialURL { hiddenWebViewDiscardManager.updateRestoredSessionRenderIntent(nil) currentURL = url shouldRenderWebView = renderInitialNavigation diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index a39f2b78048e..862c37bfb2a3 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -14905,6 +14905,7 @@ final class Workspace: Identifiable, ObservableObject { initialURL: initialURLForBrowserRole(surfaceRole, requestedURL: url, creationPolicy: creationPolicy), renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, + useSurfaceRoleDefaultInitialURL: creationPolicy != .restoration, omnibarVisible: effectiveOmnibarVisible, transparentBackground: transparentBackground, proxyEndpoint: remoteProxyEndpoint, @@ -15020,6 +15021,7 @@ final class Workspace: Identifiable, ObservableObject { renderInitialNavigation: browserEnabled || creationPolicy != .restoration, preloadInitialNavigationInBackground: creationPolicy.preloadsInitialNavigationInBackground, bypassInsecureHTTPHostOnce: bypassInsecureHTTPHostOnce, + useSurfaceRoleDefaultInitialURL: creationPolicy != .restoration, omnibarVisible: effectiveOmnibarVisible, transparentBackground: transparentBackground, proxyEndpoint: remoteProxyEndpoint, diff --git a/cmuxTests/BrowserPanelTests.swift b/cmuxTests/BrowserPanelTests.swift index 62f7157dc7a9..92e34684110a 100644 --- a/cmuxTests/BrowserPanelTests.swift +++ b/cmuxTests/BrowserPanelTests.swift @@ -1068,6 +1068,18 @@ final class BrowserPanelReactGrabBridgeTests: XCTestCase { XCTAssertEqual(panel.preferredFocusIntent, .addressBar) } + func testCodeEditorRoleDefaultsInitialURLAndHidesOmnibar() { + let panel = BrowserPanel( + workspaceId: UUID(), + surfaceRole: .codeEditor, + renderInitialNavigation: false + ) + defer { panel.close() } + + XCTAssertEqual(panel.currentURL?.absoluteString, "https://vscode.dev/") + XCTAssertFalse(panel.isOmnibarVisible) + } + func testCopySuccessPostsPastebackNotificationAndClearsPendingTarget() throws { let workspaceId = UUID() let terminalId = UUID() From 0d93de6d47d9b5a6022e30203089f8b068cac5da Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 14 Jun 2026 13:07:15 -0700 Subject: [PATCH 15/18] chore: refresh Swift file length budget --- .github/swift-file-length-budget.tsv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/swift-file-length-budget.tsv b/.github/swift-file-length-budget.tsv index e3a0d9a613a5..d970a11077b4 100644 --- a/.github/swift-file-length-budget.tsv +++ b/.github/swift-file-length-budget.tsv @@ -3,7 +3,7 @@ # Reduce counts as files shrink. CI fails if tracked files exceed this budget. 33857 CLI/cmux.swift 18065 Sources/AppDelegate.swift -16793 Sources/ContentView.swift +16762 Sources/ContentView.swift 14612 Sources/TerminalController.swift 13675 Sources/Panels/BrowserPanel.swift 12166 Sources/Workspace.swift @@ -13,8 +13,8 @@ 7915 Sources/Panels/BrowserPanelView.swift 7428 cmuxTests/WorkspaceUnitTests.swift 6944 cmuxTests/WorkspaceRemoteConnectionTests.swift +6363 cmuxTests/GhosttyConfigTests.swift 6317 cmuxTests/SessionPersistenceTests.swift -6299 cmuxTests/GhosttyConfigTests.swift 6192 Sources/TabManager.swift 6153 CLI/cmux_open.swift 6074 Sources/TextBoxInput.swift From cdba88c79a3c8025e6895d2d0b63250fc44d2cef Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 14 Jun 2026 13:19:47 -0700 Subject: [PATCH 16/18] fix: handle code editor canvas icon --- Sources/Canvas/WorkspaceCanvasHostView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Canvas/WorkspaceCanvasHostView.swift b/Sources/Canvas/WorkspaceCanvasHostView.swift index a20b045fc3f6..132735a8ca46 100644 --- a/Sources/Canvas/WorkspaceCanvasHostView.swift +++ b/Sources/Canvas/WorkspaceCanvasHostView.swift @@ -65,6 +65,7 @@ struct WorkspaceCanvasHostView: View { switch panelType { case .terminal: return "terminal" case .browser: return "globe" + case .codeEditor: return "curlybraces.square" case .markdown: return "doc.richtext" case .filePreview: return "doc.text.magnifyingglass" case .rightSidebarTool: return "sidebar.right" From 391cd0a920f690dc3325968fcbe5423d956b7f66 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 14 Jun 2026 14:09:06 -0700 Subject: [PATCH 17/18] fix: stabilize code editor async handling --- .../RemotePlatformProbeScriptTests.swift | 32 +++---- .../RemoteProcessPipeTestGate.swift | 18 ++++ .../RemoteSessionProcessRunnerTests.swift | 84 +++++++++++-------- .../App/TerminalDirectoryOpenSupport.swift | 25 +++--- Sources/AppDelegate.swift | 8 +- 5 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteProcessPipeTestGate.swift diff --git a/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePlatformProbeScriptTests.swift b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePlatformProbeScriptTests.swift index 951ed3eadbf5..950deb131399 100644 --- a/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePlatformProbeScriptTests.swift +++ b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemotePlatformProbeScriptTests.swift @@ -195,20 +195,22 @@ struct RemotePlatformProbeScriptTests { } private static func runProcess(executablePath: String, arguments: [String]) throws -> ProcessResult { - let process = Process() - let stdoutPipe = Pipe() - let stderrPipe = Pipe() - process.executableURL = URL(fileURLWithPath: executablePath) - process.arguments = arguments - process.standardInput = FileHandle.nullDevice - process.standardOutput = stdoutPipe - process.standardError = stderrPipe - - try process.run() - process.waitUntilExit() - - let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" - return ProcessResult(status: process.terminationStatus, stdout: stdout, stderr: stderr) + try RemoteProcessPipeTestGate.shared.run { + let process = Process() + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + process.executableURL = URL(fileURLWithPath: executablePath) + process.arguments = arguments + process.standardInput = FileHandle.nullDevice + process.standardOutput = stdoutPipe + process.standardError = stderrPipe + + try process.run() + process.waitUntilExit() + + let stdout = String(data: stdoutPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + let stderr = String(data: stderrPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" + return ProcessResult(status: process.terminationStatus, stdout: stdout, stderr: stderr) + } } } diff --git a/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteProcessPipeTestGate.swift b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteProcessPipeTestGate.swift new file mode 100644 index 000000000000..105102c64815 --- /dev/null +++ b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteProcessPipeTestGate.swift @@ -0,0 +1,18 @@ +import Foundation + +// Test-only process/pipe serialization. These suites deliberately close pipe +// read handles; a shared gate keeps Swift Testing's cross-suite parallelism from +// recycling those descriptors into a sibling process capture. +final class RemoteProcessPipeTestGate: @unchecked Sendable { + static let shared = RemoteProcessPipeTestGate() + + private let lock = NSLock() + + private init() {} + + func run(_ body: () throws -> T) rethrows -> T { + lock.lock() + defer { lock.unlock() } + return try body() + } +} diff --git a/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift index 640a3c5ef8b0..9c444f6761ad 100644 --- a/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift +++ b/Packages/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift @@ -27,10 +27,12 @@ struct RemoteSessionProcessRunnerTests { didCloseReadHandles.signal() }) - let result = try runner.run( - RemoteProcessRequest(executable: "/usr/bin/true", arguments: [], timeout: 2), - operation: nil - ) + let result = try RemoteProcessPipeTestGate.shared.run { + try runner.run( + RemoteProcessRequest(executable: "/usr/bin/true", arguments: [], timeout: 2), + operation: nil + ) + } #expect(didCloseReadHandles.wait(timeout: .now() + 2) == .success) #expect(result.status == 0) @@ -41,14 +43,16 @@ struct RemoteSessionProcessRunnerTests { @Test("Captures stdout, stderr, and the exit status") func capturesOutputAndStatus() throws { let runner = RemoteSessionProcessRunner() - let result = try runner.run( - RemoteProcessRequest( - executable: "/bin/sh", - arguments: ["-c", "printf out; printf err 1>&2; exit 3"], - timeout: 5 - ), - operation: nil - ) + let result = try RemoteProcessPipeTestGate.shared.run { + try runner.run( + RemoteProcessRequest( + executable: "/bin/sh", + arguments: ["-c", "printf out; printf err 1>&2; exit 3"], + timeout: 5 + ), + operation: nil + ) + } #expect(result.status == 3) #expect(result.stdout == "out") #expect(result.stderr == "err") @@ -57,15 +61,17 @@ struct RemoteSessionProcessRunnerTests { @Test("Delivers stdin and closes the write end") func deliversStdin() throws { let runner = RemoteSessionProcessRunner() - let result = try runner.run( - RemoteProcessRequest( - executable: "/bin/cat", - arguments: [], - stdin: Data("hello-stdin".utf8), - timeout: 5 - ), - operation: nil - ) + let result = try RemoteProcessPipeTestGate.shared.run { + try runner.run( + RemoteProcessRequest( + executable: "/bin/cat", + arguments: [], + stdin: Data("hello-stdin".utf8), + timeout: 5 + ), + operation: nil + ) + } #expect(result.status == 0) #expect(result.stdout == "hello-stdin") } @@ -74,14 +80,16 @@ struct RemoteSessionProcessRunnerTests { func launchFailurePinsErrorCode() { let runner = RemoteSessionProcessRunner() #expect { - try runner.run( - RemoteProcessRequest( - executable: "/nonexistent/cmux-no-such-binary", - arguments: [], - timeout: 2 - ), - operation: nil - ) + try RemoteProcessPipeTestGate.shared.run { + try runner.run( + RemoteProcessRequest( + executable: "/nonexistent/cmux-no-such-binary", + arguments: [], + timeout: 2 + ), + operation: nil + ) + } } throws: { error in let nsError = error as NSError return nsError.domain == "cmux.remote.process" @@ -94,14 +102,16 @@ struct RemoteSessionProcessRunnerTests { func timeoutPinsErrorCode() { let runner = RemoteSessionProcessRunner() #expect { - try runner.run( - RemoteProcessRequest( - executable: "/bin/sh", - arguments: ["-c", "sleep 30"], - timeout: 1 - ), - operation: nil - ) + try RemoteProcessPipeTestGate.shared.run { + try runner.run( + RemoteProcessRequest( + executable: "/bin/sh", + arguments: ["-c", "sleep 30"], + timeout: 1 + ), + operation: nil + ) + } } throws: { error in let nsError = error as NSError return nsError.domain == "cmux.remote.process" diff --git a/Sources/App/TerminalDirectoryOpenSupport.swift b/Sources/App/TerminalDirectoryOpenSupport.swift index 46a9edde080b..b792a0808212 100644 --- a/Sources/App/TerminalDirectoryOpenSupport.swift +++ b/Sources/App/TerminalDirectoryOpenSupport.swift @@ -561,6 +561,8 @@ final class VSCodeServeWebController { guard self.activeLaunchGeneration == launchGeneration else { return } self.isLaunching = false self.activeLaunchGeneration = nil + let completions = self.takePendingCompletions(for: launchGeneration) + DispatchQueue.main.async { completions.forEach { $0(nil) } } } return } @@ -586,6 +588,8 @@ final class VSCodeServeWebController { if let process = launchResult?.process, process.isRunning { process.terminate() } + let completions = self.takePendingCompletions(for: launchGeneration) + DispatchQueue.main.async { completions.forEach { $0(nil) } } return } @@ -599,25 +603,20 @@ final class VSCodeServeWebController { self.serveWebURL = nil } - var completions: [(URL?) -> Void] = [] - var remaining: [(generation: UInt64, completion: (URL?) -> Void)] = [] - for pending in self.pendingCompletions { - if pending.generation == launchGeneration { - completions.append(pending.completion) - } else { - remaining.append(pending) - } - } - self.pendingCompletions = remaining + let completions = self.takePendingCompletions(for: launchGeneration) let resolvedURL = self.serveWebURL - DispatchQueue.main.async { - completions.forEach { $0(resolvedURL) } - } + DispatchQueue.main.async { completions.forEach { $0(resolvedURL) } } } } } } + private func takePendingCompletions(for generation: UInt64) -> [(URL?) -> Void] { + let completions = pendingCompletions.filter { $0.generation == generation }.map(\.completion) + pendingCompletions.removeAll { $0.generation == generation } + return completions + } + func ensureServeWebURL(vscodeApplicationURL: URL) async -> URL? { await withCheckedContinuation { continuation in ensureServeWebURL(vscodeApplicationURL: vscodeApplicationURL) { serveWebURL in diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 994231ca6a4a..113db7493dc0 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -7271,10 +7271,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent let targetWorkspaceId = targetWorkspace.id let normalizedDirectoryURL = directoryURL.standardizedFileURL - Task { @MainActor in + Task { @MainActor [weak targetTabManager] in let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( vscodeApplicationURL: vscodeApplicationURL ) + guard !Task.isCancelled, let targetTabManager else { return } guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, @@ -7293,7 +7294,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return } } - return true } @@ -7350,10 +7350,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent return nil } - Task { @MainActor in + Task { @MainActor [weak targetTabManager] in let serveWebURL = await VSCodeServeWebController.shared.ensureServeWebURL( vscodeApplicationURL: vscodeApplicationURL ) + guard !Task.isCancelled, let targetTabManager else { return } guard let serveWebURL, let openFolderURL = VSCodeServeWebURLBuilder.openFolderURL( baseWebUIURL: serveWebURL, @@ -7376,7 +7377,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent )? .navigate(to: openFolderURL) } - return panelId } From 4bde2d709662aebda0b95176b10578c7232cb165 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Thu, 18 Jun 2026 12:22:49 -0700 Subject: [PATCH 18/18] fix: use raw surface kind for editor tab action --- Sources/Workspace.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 887bdc1accc6..5c088eb4fef0 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -12618,7 +12618,7 @@ extension Workspace: BonsplitDelegate { _ = newTerminalSurface(inPane: pane, inheritWorkingDirectoryFallback: true) case "browser": _ = newBrowserSurface(inPane: pane) - case SurfaceKind.codeEditor, "codeEditor", "code_editor": + case SurfaceKind.codeEditor.rawValue, "codeEditor", "code_editor": _ = openCodeEditorFromPane(pane) default: _ = newTerminalSurface(inPane: pane, inheritWorkingDirectoryFallback: true)