From 95f7850f7f11c860c9f2292421c865402a93ef56 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 11 May 2026 19:30:31 -0700 Subject: [PATCH 1/8] Make new surface placement configurable Surface creation had no cmux-owned insertion policy, so user-facing entry points inherited Bonsplit's implicit after-current behavior. Add app.newSurfacePlacement as the sibling to workspace placement and route terminal, browser, markdown, and file-preview surface creation through one Workspace placement decision while preserving explicit restore/config ordering. Constraint: Do not run local tests or xcodebuild for this task; CI owns test execution. Constraint: Existing Bonsplit createTab only exposes current/end behavior, so cmux applies start/end policies as a post-create reorder before focus reconciliation. Rejected: Configure Bonsplit globally | it cannot express top/start and would affect restoration/configuration paths that need explicit ordering. Confidence: medium Scope-risk: moderate Directive: Keep user-initiated surface creation on the shared Workspace placement path; pass explicit placement overrides for restore/config flows that must preserve stored order. Tested: git diff --check; jq empty Resources/Localizable.xcstrings; jq empty web/data/cmux.schema.json Not-tested: Local unit/UI tests and local app build per user instruction; CI pending. --- Resources/Localizable.xcstrings | 136 ++++++++++++ Sources/CmuxSettingsJSONPathSupport.swift | 1 + ...rdShortcutSettingsFileStore+Template.swift | 1 + .../KeyboardShortcutSettingsFileStore.swift | 7 + Sources/SettingsNavigation.swift | 2 + Sources/SettingsSearchAliases.swift | 1 + Sources/TabManager.swift | 80 +++++++ Sources/Workspace.swift | 140 ++++++++++-- Sources/cmuxApp.swift | 20 ++ cmuxTests/WorkspaceUnitTests.swift | 208 +++++++++++++++++- web/app/[locale]/docs/configuration/page.tsx | 1 + web/data/cmux.schema.json | 6 + 12 files changed, 578 insertions(+), 25 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index f51105c327ad..5bd38f303a5a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -52489,6 +52489,23 @@ } } }, + "settings.app.newSurfacePlacement": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "New Surface Placement" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新規サーフェスの配置" + } + } + } + }, "settings.app.settingsFile.openButton": { "extractionState": "manual", "localizations": { @@ -72177,6 +72194,23 @@ } } }, + "settings.search.alias.setting.app.new-surface-placement": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current 新規サーフェス 新しいタブ 挿入位置 並び順 先頭 末尾 現在の後" + } + } + } + }, "settings.search.alias.setting.app.minimal-mode": { "extractionState": "manual", "localizations": { @@ -100105,6 +100139,108 @@ } } }, + "surface.placement.afterCurrent": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "After current" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "現在の後" + } + } + } + }, + "surface.placement.afterCurrent.description": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Insert new surfaces directly after the active surface." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アクティブなサーフェスの直後に新しいサーフェスを挿入します。" + } + } + } + }, + "surface.placement.end": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "End" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "末尾" + } + } + } + }, + "surface.placement.end.description": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Append new surfaces to the end of the pane's tab strip." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新しいサーフェスをペインのタブ列の末尾に追加します。" + } + } + } + }, + "surface.placement.top": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "先頭" + } + } + } + }, + "surface.placement.top.description": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Insert new surfaces at the start of the pane's tab strip." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "新しいサーフェスをペインのタブ列の先頭に挿入します。" + } + } + } + }, "workspace.placement.afterCurrent": { "extractionState": "manual", "localizations": { diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index f40f1fd297db..5930e8f481da 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -24,6 +24,7 @@ extension CmuxSettingsFileStore { "app.appIcon", "app.menuBarOnly", "app.newWorkspacePlacement", + "app.newSurfacePlacement", "app.minimalMode", "app.keepWorkspaceOpenWhenClosingLastSurface", "app.focusPaneOnFirstClick", diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index 066656c88316..cb8a81ec7eb9 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -61,6 +61,7 @@ extension CmuxSettingsFileStore { "appIcon": AppIconSettings.defaultMode.rawValue, "menuBarOnly": MenuBarOnlySettings.defaultMenuBarOnly, "newWorkspacePlacement": WorkspacePlacementSettings.defaultPlacement.rawValue, + "newSurfacePlacement": SurfacePlacementSettings.defaultPlacement.rawValue, "minimalMode": false, "keepWorkspaceOpenWhenClosingLastSurface": !LastSurfaceCloseShortcutSettings.defaultValue, "focusPaneOnFirstClick": PaneFirstClickFocusSettings.defaultEnabled, diff --git a/Sources/KeyboardShortcutSettingsFileStore.swift b/Sources/KeyboardShortcutSettingsFileStore.swift index 0ed9252c47bc..356998f7e120 100644 --- a/Sources/KeyboardShortcutSettingsFileStore.swift +++ b/Sources/KeyboardShortcutSettingsFileStore.swift @@ -374,6 +374,13 @@ final class CmuxSettingsFileStore { } snapshot.managedUserDefaults[WorkspacePlacementSettings.placementKey] = .string(placement.rawValue) } + if let raw = jsonString(section["newSurfacePlacement"]) { + guard let placement = NewSurfacePlacement(rawValue: raw) else { + logInvalid("app.newSurfacePlacement", sourcePath: sourcePath) + return + } + snapshot.managedUserDefaults[SurfacePlacementSettings.placementKey] = .string(placement.rawValue) + } if let value = jsonBool(section["minimalMode"]) { let mode = value ? WorkspacePresentationModeSettings.Mode.minimal : .standard snapshot.managedUserDefaults[WorkspacePresentationModeSettings.modeKey] = .string(mode.rawValue) diff --git a/Sources/SettingsNavigation.swift b/Sources/SettingsNavigation.swift index 6d2c4943b768..921a79946ade 100644 --- a/Sources/SettingsNavigation.swift +++ b/Sources/SettingsNavigation.swift @@ -293,6 +293,7 @@ enum SettingsSearchIndex { setting(.app, "appearance", String(localized: "settings.app.appearance", defaultValue: "Appearance"), "theme light dark system"), setting(.app, "app-icon", String(localized: "settings.app.appIcon", defaultValue: "App Icon"), "dock icon alternate"), setting(.app, "new-workspace-placement", String(localized: "settings.app.newWorkspacePlacement", defaultValue: "New Workspace Placement"), "workspace order position"), + setting(.app, "new-surface-placement", String(localized: "settings.app.newSurfacePlacement", defaultValue: "New Surface Placement"), "surface tab order position pane"), setting(.app, "minimal-mode", String(localized: "settings.app.minimalMode", defaultValue: "Minimal Mode"), "presentation compact chrome"), setting(.app, "keep-workspace-open", String(localized: "settings.app.closeWorkspaceOnLastSurfaceShortcut", defaultValue: "Keep Workspace Open When Closing Last Surface"), "close last surface shortcut"), setting(.app, "focus-pane-first-click", String(localized: "settings.app.paneFirstClickFocus", defaultValue: "Focus Pane on First Click"), "mouse click focus"), @@ -378,6 +379,7 @@ enum SettingsSearchIndex { "app.appearance": settingID(for: .app, idSuffix: "appearance"), "app.appIcon": settingID(for: .app, idSuffix: "app-icon"), "app.newWorkspacePlacement": settingID(for: .app, idSuffix: "new-workspace-placement"), + "app.newSurfacePlacement": settingID(for: .app, idSuffix: "new-surface-placement"), "app.minimalMode": settingID(for: .app, idSuffix: "minimal-mode"), "app.keepWorkspaceOpenWhenClosingLastSurface": settingID(for: .app, idSuffix: "keep-workspace-open"), "app.focusPaneOnFirstClick": settingID(for: .app, idSuffix: "focus-pane-first-click"), diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index 35a435cabe44..7adb0b4b87d8 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -44,6 +44,7 @@ enum SettingsSearchAliasIndex { "app:appearance": localized("settings.search.alias.setting.app.appearance", defaultValue: "app.appearance theme color scheme light mode dark mode system mode"), "app:app-icon": localized("settings.search.alias.setting.app.app-icon", defaultValue: "app.appIcon dock icon application icon app switcher alternate icon"), "app:new-workspace-placement": localized("settings.search.alias.setting.app.new-workspace-placement", defaultValue: "app.newWorkspacePlacement new tab insert position order top bottom end"), + "app:new-surface-placement": localized("settings.search.alias.setting.app.new-surface-placement", defaultValue: "app.newSurfacePlacement surface tab insert position order start end after current"), "app:minimal-mode": localized("settings.search.alias.setting.app.minimal-mode", defaultValue: "app.minimalMode minimal layout simple chrome compact titlebar controls"), "app:keep-workspace-open": localized("settings.search.alias.setting.app.keep-workspace-open", defaultValue: "app.keepWorkspaceOpenWhenClosingLastSurface close last pane surface keep tab workspace"), "app:focus-pane-first-click": localized("settings.search.alias.setting.app.focus-pane-first-click", defaultValue: "app.focusPaneOnFirstClick click to focus focus follows mouse first click mouse activation"), diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index 5b607c515a8f..e0f186251884 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -48,6 +48,45 @@ enum NewWorkspacePlacement: String, CaseIterable, Identifiable { } } +enum NewSurfacePlacement: String, CaseIterable, Identifiable { + case top + case afterCurrent + case end + + var id: String { rawValue } + + var displayName: String { + switch self { + case .top: + return String(localized: "surface.placement.top", defaultValue: "Start") + case .afterCurrent: + return String(localized: "surface.placement.afterCurrent", defaultValue: "After current") + case .end: + return String(localized: "surface.placement.end", defaultValue: "End") + } + } + + var description: String { + switch self { + case .top: + return String( + localized: "surface.placement.top.description", + defaultValue: "Insert new surfaces at the start of the pane's tab strip." + ) + case .afterCurrent: + return String( + localized: "surface.placement.afterCurrent.description", + defaultValue: "Insert new surfaces directly after the active surface." + ) + case .end: + return String( + localized: "surface.placement.end.description", + defaultValue: "Append new surfaces to the end of the pane's tab strip." + ) + } + } +} + enum WorkspaceAutoReorderSettings { static let key = "workspaceAutoReorderOnNotification" static let defaultValue = true @@ -247,6 +286,47 @@ enum WorkspacePlacementSettings { } } +enum SurfacePlacementSettings { + static let placementKey = "newSurfacePlacement" + static let defaultPlacement: NewSurfacePlacement = .afterCurrent + + static func current(defaults: UserDefaults = .standard) -> NewSurfacePlacement { + guard let raw = defaults.string(forKey: placementKey), + let placement = NewSurfacePlacement(rawValue: raw) else { + return defaultPlacement + } + return placement + } + + static func insertionIndex( + placement: NewSurfacePlacement, + selectedIndexBeforeCreation: Int?, + selectedWasPinned: Bool, + pinnedCount: Int, + totalCountAfterCreation: Int + ) -> Int { + let clampedTotalCount = max(0, totalCountAfterCreation) + let clampedPinnedCount = max(0, min(pinnedCount, clampedTotalCount)) + let lastIndex = max(0, clampedTotalCount - 1) + + switch placement { + case .top: + return clampedPinnedCount + case .end: + return lastIndex + case .afterCurrent: + guard let selectedIndexBeforeCreation, clampedTotalCount > 0 else { + return lastIndex + } + if selectedWasPinned { + return clampedPinnedCount + } + let clampedSelectedIndex = max(0, min(selectedIndexBeforeCreation, lastIndex)) + return max(min(clampedSelectedIndex + 1, lastIndex), clampedPinnedCount) + } + } +} + struct WorkspaceTabColorEntry: Equatable, Identifiable { let name: String let hex: String diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 18950839e669..ccafedf36a65 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -658,7 +658,7 @@ extension Workspace { .first if anchorPanelId == nil { - anchorPanelId = newTerminalSurface(inPane: paneId, focus: false)?.id + anchorPanelId = newTerminalSurface(inPane: paneId, focus: false, placementOverride: .end)?.id } guard let anchorPanelId, @@ -778,7 +778,8 @@ extension Workspace { initialCommand: restoredTmuxStartupScript?.path, tmuxStartCommand: restoredTmuxStartCommand, initialInput: restoredAgentResumeInput, - startupEnvironment: replayEnvironment + startupEnvironment: replayEnvironment, + placementOverride: .end ) else { return nil } @@ -811,7 +812,8 @@ extension Workspace { url: nil, focus: false, preferredProfileID: snapshot.browser?.profileID, - creationPolicy: .restoration + creationPolicy: .restoration, + placementOverride: .end ) else { return nil } @@ -822,7 +824,8 @@ extension Workspace { let markdownPanel = newMarkdownSurface( inPane: paneId, filePath: filePath, - focus: false + focus: false, + placementOverride: .end ) else { return nil } @@ -833,7 +836,8 @@ extension Workspace { let filePreviewPanel = newFilePreviewSurface( inPane: paneId, filePath: filePath, - focus: false + focus: false, + placementOverride: .end ) else { return nil } @@ -962,7 +966,7 @@ extension Workspace { .first if anchorPanelId == nil { - anchorPanelId = newTerminalSurface(inPane: paneId, focus: false)?.id + anchorPanelId = newTerminalSurface(inPane: paneId, focus: false, placementOverride: .end)?.id } guard let anchorPanelId, @@ -1030,7 +1034,8 @@ extension Workspace { inPane: paneId, focus: false, workingDirectory: resolvedCwd, - startupEnvironment: surface.env ?? [:] + startupEnvironment: surface.env ?? [:], + placementOverride: .end ) { _ = closePanel(panelId, force: true) if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } @@ -1051,7 +1056,8 @@ extension Workspace { inPane: paneId, url: url, focus: false, - creationPolicy: .restoration + creationPolicy: .restoration, + placementOverride: .end ) { _ = closePanel(panelId, force: true) if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } @@ -1073,7 +1079,8 @@ extension Workspace { inPane: paneId, focus: false, workingDirectory: resolvedCwd, - startupEnvironment: surface.env ?? [:] + startupEnvironment: surface.env ?? [:], + placementOverride: .end ) { if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } if surface.focus == true { focusPanelId = panel.id } @@ -1086,7 +1093,8 @@ extension Workspace { inPane: paneId, url: url, focus: false, - creationPolicy: .restoration + creationPolicy: .restoration, + placementOverride: .end ) { if let name = surface.name { setPanelCustomTitle(panelId: panel.id, title: name) } if surface.focus == true { focusPanelId = panel.id } @@ -8206,6 +8214,49 @@ final class Workspace: Identifiable, ObservableObject { } } + private struct SurfaceSelectionBeforeCreation { + let index: Int? + let wasPinned: Bool + } + + private func surfaceSelectionBeforeCreation(inPane paneId: PaneID) -> SurfaceSelectionBeforeCreation { + let tabs = bonsplitController.tabs(inPane: paneId) + guard let selectedTabId = bonsplitController.selectedTab(inPane: paneId)?.id, + let selectedIndex = tabs.firstIndex(where: { $0.id == selectedTabId }) else { + return SurfaceSelectionBeforeCreation(index: nil, wasPinned: false) + } + let selectedPanelId = panelIdFromSurfaceId(selectedTabId) + return SurfaceSelectionBeforeCreation( + index: selectedIndex, + wasPinned: selectedPanelId.map { pinnedPanelIds.contains($0) } ?? false + ) + } + + private func applyNewSurfacePlacement( + to newTabId: TabID, + inPane paneId: PaneID, + selectionBeforeCreation: SurfaceSelectionBeforeCreation, + placementOverride: NewSurfacePlacement? = nil + ) { + let placement = placementOverride ?? SurfacePlacementSettings.current() + let tabs = bonsplitController.tabs(inPane: paneId) + guard let currentIndex = tabs.firstIndex(where: { $0.id == newTabId }) else { return } + let pinnedCount = tabs.reduce(into: 0) { count, tab in + if let panelId = panelIdFromSurfaceId(tab.id), pinnedPanelIds.contains(panelId) { + count += 1 + } + } + let targetIndex = SurfacePlacementSettings.insertionIndex( + placement: placement, + selectedIndexBeforeCreation: selectionBeforeCreation.index, + selectedWasPinned: selectionBeforeCreation.wasPinned, + pinnedCount: pinnedCount, + totalCountAfterCreation: tabs.count + ) + guard currentIndex != targetIndex else { return } + _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) + } + private func insertionIndexToRight(of anchorTabId: TabID, inPane paneId: PaneID) -> Int { let tabs = bonsplitController.tabs(inPane: paneId) guard let anchorIndex = tabs.firstIndex(where: { $0.id == anchorTabId }) else { return tabs.count } @@ -9967,9 +10018,11 @@ final class Workspace: Identifiable, ObservableObject { initialCommand: String? = nil, tmuxStartCommand: String? = nil, initialInput: String? = nil, - startupEnvironment: [String: String] = [:] + startupEnvironment: [String: String] = [:], + placementOverride: NewSurfacePlacement? = nil ) -> TerminalPanel? { let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId) + let selectionBeforeCreation = surfaceSelectionBeforeCreation(inPane: paneId) let previousFocusedPanelId = focusedPanelId let previousHostedView = focusedTerminalPanel?.hostedView @@ -10026,6 +10079,12 @@ final class Workspace: Identifiable, ObservableObject { } surfaceIdToPanelId[newTabId] = newPanel.id + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: placementOverride + ) publishCmuxSurfaceCreated(newPanel.id, paneId: paneId, kind: "terminal", origin: "terminal_tab", focused: shouldFocusNewTab) // bonsplit's createTab may not reliably emit didSelectTab, and its internal selection @@ -10171,7 +10230,8 @@ final class Workspace: Identifiable, ObservableObject { insertAtEnd: Bool = false, preferredProfileID: UUID? = nil, bypassInsecureHTTPHostOnce: String? = nil, - creationPolicy: BrowserPanelCreationPolicy = .userInitiated + creationPolicy: BrowserPanelCreationPolicy = .userInitiated, + placementOverride: NewSurfacePlacement? = nil ) -> BrowserPanel? { let browserEnabled = BrowserAvailabilitySettings.isEnabled() guard browserEnabled || creationPolicy.permitsCreationWhenBrowserDisabled else { @@ -10182,6 +10242,7 @@ final class Workspace: Identifiable, ObservableObject { } let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId) + let selectionBeforeCreation = surfaceSelectionBeforeCreation(inPane: paneId) let sourcePanelId = effectiveSelectedPanelId(inPane: paneId) let previousFocusedPanelId = focusedPanelId let previousHostedView = focusedTerminalPanel?.hostedView @@ -10219,12 +10280,12 @@ final class Workspace: Identifiable, ObservableObject { surfaceIdToPanelId[newTabId] = browserPanel.id setPreferredBrowserProfileID(browserPanel.profileID) - - // Keyboard/browser-open paths want "new tab at end" regardless of global new-tab placement. - if insertAtEnd { - let targetIndex = max(0, bonsplitController.tabs(inPane: paneId).count - 1) - _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) - } + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: insertAtEnd ? .end : placementOverride + ) publishCmuxSurfaceCreated(browserPanel.id, paneId: paneId, kind: "browser", origin: "browser_tab", focused: shouldFocusNewTab) // Match terminal behavior: enforce deterministic selection + focus. @@ -10342,9 +10403,11 @@ final class Workspace: Identifiable, ObservableObject { func newMarkdownSurface( inPane paneId: PaneID, filePath: String, - focus: Bool? = nil + focus: Bool? = nil, + placementOverride: NewSurfacePlacement? = nil ) -> MarkdownPanel? { let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId) + let selectionBeforeCreation = surfaceSelectionBeforeCreation(inPane: paneId) let previousFocusedPanelId = focusedPanelId let previousHostedView = focusedTerminalPanel?.hostedView @@ -10367,6 +10430,12 @@ final class Workspace: Identifiable, ObservableObject { } surfaceIdToPanelId[newTabId] = markdownPanel.id + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: placementOverride + ) publishCmuxSurfaceCreated(markdownPanel.id, paneId: paneId, kind: "markdown", origin: "markdown_tab", focused: shouldFocusNewTab) if shouldFocusNewTab { bonsplitController.focusPane(paneId) @@ -10409,9 +10478,11 @@ final class Workspace: Identifiable, ObservableObject { inPane paneId: PaneID, filePath: String, focus: Bool? = nil, - targetIndex: Int? = nil + targetIndex: Int? = nil, + placementOverride: NewSurfacePlacement? = nil ) -> FilePreviewPanel? { let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId) + let selectionBeforeCreation = surfaceSelectionBeforeCreation(inPane: paneId) let previousFocusedPanelId = focusedPanelId let previousHostedView = focusedTerminalPanel?.hostedView @@ -10436,6 +10507,13 @@ final class Workspace: Identifiable, ObservableObject { surfaceIdToPanelId[newTabId] = filePreviewPanel.id if let targetIndex { _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) + } else { + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: placementOverride + ) } publishCmuxSurfaceCreated(filePreviewPanel.id, paneId: paneId, kind: "file_preview", origin: "file_preview_tab", focused: shouldFocusNewTab) if shouldFocusNewTab { @@ -12393,6 +12471,14 @@ final class Workspace: Identifiable, ObservableObject { _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) } + private func createTerminalFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID) { + guard SurfacePlacementSettings.current() != .afterCurrent else { + createTerminalToRight(of: anchorTabId, inPane: paneId) + return + } + _ = newTerminalSurface(inPane: paneId, focus: true) + } + private func createBrowserToRight(of anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) let preferredProfileID = panelIdFromSurfaceId(anchorTabId).flatMap { browserPanel(for: $0)?.profileID } @@ -12405,6 +12491,14 @@ final class Workspace: Identifiable, ObservableObject { _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) } + private func createBrowserFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { + guard SurfacePlacementSettings.current() != .afterCurrent else { + createBrowserToRight(of: anchorTabId, inPane: paneId, url: url) + return + } + _ = newBrowserSurface(inPane: paneId, url: url, focus: true) + } + private func duplicateBrowserToRight(anchorTabId: TabID, inPane paneId: PaneID) { guard let panelId = panelIdFromSurfaceId(anchorTabId), let browser = browserPanel(for: panelId) else { return } @@ -13668,7 +13762,7 @@ extension Workspace: BonsplitDelegate { "fallback=createTerminalAndDropPlaceholders" ) #endif - _ = newTerminalSurface(inPane: originalPane, focus: false) + _ = newTerminalSurface(inPane: originalPane, focus: false, placementOverride: .end) for tab in controller.tabs(inPane: originalPane) { if panelIdFromSurfaceId(tab.id) == nil { bonsplitController.closeTab(tab.id) @@ -13895,9 +13989,9 @@ extension Workspace: BonsplitDelegate { guard let panelId = panelIdFromSurfaceId(tab.id) else { return } _ = moveSurfaceToAdjacentPane(panelId: panelId, direction: .right) case .newTerminalToRight: - createTerminalToRight(of: tab.id, inPane: pane) + createTerminalFromContextMenu(anchorTabId: tab.id, inPane: pane) case .newBrowserToRight: - createBrowserToRight(of: tab.id, inPane: pane) + createBrowserFromContextMenu(anchorTabId: tab.id, inPane: pane) case .reload: guard let panelId = panelIdFromSurfaceId(tab.id), let browser = browserPanel(for: panelId) else { return } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index b7b8e94170cd..3ee0fbb144f2 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -5005,6 +5005,7 @@ struct SettingsView: View { @AppStorage(CommandPaletteSwitcherSearchSettings.searchAllSurfacesKey) private var commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces @AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue + @AppStorage(SurfacePlacementSettings.placementKey) private var newSurfacePlacement = SurfacePlacementSettings.defaultPlacement.rawValue @AppStorage(LastSurfaceCloseShortcutSettings.key) private var closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue @AppStorage(PaneFirstClickFocusSettings.enabledKey) @@ -5074,6 +5075,10 @@ struct SettingsView: View { NewWorkspacePlacement(rawValue: newWorkspacePlacement) ?? WorkspacePlacementSettings.defaultPlacement } + private var selectedSurfacePlacement: NewSurfacePlacement { + NewSurfacePlacement(rawValue: newSurfacePlacement) ?? SurfacePlacementSettings.defaultPlacement + } + private var minimalModeEnabled: Bool { WorkspacePresentationModeSettings.mode(for: workspacePresentationMode) == .minimal } @@ -5724,6 +5729,20 @@ struct SettingsView: View { SettingsCardDivider() + SettingsPickerRow( + configurationReview: .json("app.newSurfacePlacement"), + String(localized: "settings.app.newSurfacePlacement", defaultValue: "New Surface Placement"), + subtitle: selectedSurfacePlacement.description, + controlWidth: pickerColumnWidth, + selection: $newSurfacePlacement + ) { + ForEach(NewSurfacePlacement.allCases) { placement in + Text(placement.displayName).tag(placement.rawValue) + } + } + + SettingsCardDivider() + SettingsCardRow( configurationReview: .json("app.minimalMode"), String(localized: "settings.app.minimalMode", defaultValue: "Minimal Mode"), @@ -7234,6 +7253,7 @@ struct SettingsView: View { commandPaletteRenameSelectAllOnFocus = CommandPaletteRenameSelectionSettings.defaultSelectAllOnFocus commandPaletteSearchAllSurfaces = CommandPaletteSwitcherSearchSettings.defaultSearchAllSurfaces newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue + newSurfacePlacement = SurfacePlacementSettings.defaultPlacement.rawValue workspacePresentationMode = WorkspacePresentationModeSettings.defaultMode.rawValue let defaults = UserDefaults.standard defaults.removeObject(forKey: WorkspaceTitlebarSettings.showTitlebarKey) diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 10f502246683..2433adf4f41a 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -1543,7 +1543,9 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { func testReloadConfigurationReloadsManagedAppSettingsFromSettingsFile() throws { let defaults = UserDefaults.standard let managedKey = WorkspacePlacementSettings.placementKey + let managedSurfaceKey = SurfacePlacementSettings.placementKey let previousValue = defaults.object(forKey: managedKey) + let previousSurfaceValue = defaults.object(forKey: managedSurfaceKey) let previousBackups = defaults.data(forKey: settingsFileBackupsDefaultsKey) defer { if let previousValue { @@ -1552,6 +1554,12 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { defaults.removeObject(forKey: managedKey) } + if let previousSurfaceValue { + defaults.set(previousSurfaceValue, forKey: managedSurfaceKey) + } else { + defaults.removeObject(forKey: managedSurfaceKey) + } + if let previousBackups { defaults.set(previousBackups, forKey: settingsFileBackupsDefaultsKey) } else { @@ -1560,6 +1568,7 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { } defaults.removeObject(forKey: managedKey) + defaults.removeObject(forKey: managedSurfaceKey) defaults.removeObject(forKey: settingsFileBackupsDefaultsKey) let directoryURL = try makeTemporaryDirectory() @@ -1570,7 +1579,8 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { """ { "app": { - "newWorkspacePlacement": "top" + "newWorkspacePlacement": "top", + "newSurfacePlacement": "top" } } """, @@ -1584,12 +1594,14 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { ) XCTAssertEqual(WorkspacePlacementSettings.current(), .top) + XCTAssertEqual(SurfacePlacementSettings.current(), .top) try writeSettingsFile( """ { "app": { - "newWorkspacePlacement": "end" + "newWorkspacePlacement": "end", + "newSurfacePlacement": "end" } } """, @@ -1599,6 +1611,7 @@ final class KeyboardShortcutSettingsFileStoreTests: XCTestCase { GhosttyApp.shared.reloadConfiguration(source: "test.reload_config_app_setting") XCTAssertEqual(WorkspacePlacementSettings.current(), .end) + XCTAssertEqual(SurfacePlacementSettings.current(), .end) } @MainActor @@ -2203,6 +2216,197 @@ final class WorkspacePlacementSettingsTests: XCTestCase { } } +final class SurfacePlacementSettingsTests: XCTestCase { + func testCurrentPlacementDefaultsToAfterCurrentWhenUnset() { + let suiteName = "SurfacePlacementSettingsTests.Default.\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + XCTFail("Failed to create isolated UserDefaults suite") + return + } + defer { defaults.removePersistentDomain(forName: suiteName) } + + XCTAssertEqual(SurfacePlacementSettings.current(defaults: defaults), .afterCurrent) + } + + func testCurrentPlacementReadsStoredValidValueAndFallsBackForInvalid() { + let suiteName = "SurfacePlacementSettingsTests.Stored.\(UUID().uuidString)" + guard let defaults = UserDefaults(suiteName: suiteName) else { + XCTFail("Failed to create isolated UserDefaults suite") + return + } + defer { defaults.removePersistentDomain(forName: suiteName) } + + defaults.set(NewSurfacePlacement.top.rawValue, forKey: SurfacePlacementSettings.placementKey) + XCTAssertEqual(SurfacePlacementSettings.current(defaults: defaults), .top) + + defaults.set("nope", forKey: SurfacePlacementSettings.placementKey) + XCTAssertEqual(SurfacePlacementSettings.current(defaults: defaults), .afterCurrent) + } + + func testInsertionIndexTopPreservesPinnedSurfaces() { + let index = SurfacePlacementSettings.insertionIndex( + placement: .top, + selectedIndexBeforeCreation: 4, + selectedWasPinned: false, + pinnedCount: 2, + totalCountAfterCreation: 7 + ) + XCTAssertEqual(index, 2) + } + + func testInsertionIndexAfterCurrentHandlesPinnedAndUnpinnedSelection() { + let afterUnpinned = SurfacePlacementSettings.insertionIndex( + placement: .afterCurrent, + selectedIndexBeforeCreation: 3, + selectedWasPinned: false, + pinnedCount: 2, + totalCountAfterCreation: 6 + ) + XCTAssertEqual(afterUnpinned, 4) + + let afterPinned = SurfacePlacementSettings.insertionIndex( + placement: .afterCurrent, + selectedIndexBeforeCreation: 0, + selectedWasPinned: true, + pinnedCount: 2, + totalCountAfterCreation: 6 + ) + XCTAssertEqual(afterPinned, 2) + } + + func testInsertionIndexEndAndNoSelectionAppend() { + let endIndex = SurfacePlacementSettings.insertionIndex( + placement: .end, + selectedIndexBeforeCreation: 1, + selectedWasPinned: false, + pinnedCount: 1, + totalCountAfterCreation: 5 + ) + XCTAssertEqual(endIndex, 4) + + let noSelectionIndex = SurfacePlacementSettings.insertionIndex( + placement: .afterCurrent, + selectedIndexBeforeCreation: nil, + selectedWasPinned: false, + pinnedCount: 0, + totalCountAfterCreation: 5 + ) + XCTAssertEqual(noSelectionIndex, 4) + } +} + +@MainActor +final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { + private func withSurfacePlacement(_ placement: NewSurfacePlacement, run body: () throws -> Void) rethrows { + let defaults = UserDefaults.standard + let key = SurfacePlacementSettings.placementKey + let previousValue = defaults.object(forKey: key) + defaults.set(placement.rawValue, forKey: key) + defer { + if let previousValue { + defaults.set(previousValue, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + try body() + } + + private func orderedPanelIds(in workspace: Workspace, paneId: PaneID) -> [UUID] { + workspace.bonsplitController + .tabs(inPane: paneId) + .compactMap { workspace.panelIdFromSurfaceId($0.id) } + } + + func testEndPlacementAppendsNewTerminalSurfaceToPaneEnd() throws { + try withSurfacePlacement(.end) { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) + let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + workspace.focusPanel(firstPanelId) + let createdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + XCTAssertEqual( + orderedPanelIds(in: workspace, paneId: paneId), + [firstPanelId, secondPanel.id, thirdPanel.id, createdPanel.id] + ) + XCTAssertEqual(workspace.focusedPanelId, createdPanel.id) + } + } + + func testContextMenuNewTerminalUsesEndPlacement() throws { + try withSurfacePlacement(.end) { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) + let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let firstTabId = try XCTUnwrap(workspace.surfaceIdFromPanelId(firstPanelId)) + let firstTab = try XCTUnwrap(workspace.bonsplitController.tab(firstTabId)) + + workspace.splitTabBar( + workspace.bonsplitController, + didRequestTabContextAction: .newTerminalToRight, + for: firstTab, + inPane: paneId + ) + + let order = orderedPanelIds(in: workspace, paneId: paneId) + let createdPanelId = try XCTUnwrap( + order.first { ![firstPanelId, secondPanel.id, thirdPanel.id].contains($0) } + ) + XCTAssertEqual(order, [firstPanelId, secondPanel.id, thirdPanel.id, createdPanelId]) + XCTAssertEqual(workspace.focusedPanelId, createdPanelId) + } + } + + func testDefaultPlacementInsertsNewTerminalSurfaceAfterFocusedSurface() throws { + try withSurfacePlacement(.afterCurrent) { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) + let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + workspace.focusPanel(firstPanelId) + let createdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + XCTAssertEqual( + orderedPanelIds(in: workspace, paneId: paneId), + [firstPanelId, createdPanel.id, secondPanel.id, thirdPanel.id] + ) + XCTAssertEqual(workspace.focusedPanelId, createdPanel.id) + } + } + + func testTopPlacementInsertsUnpinnedSurfaceAfterPinnedSurfaces() throws { + try withSurfacePlacement(.end) { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) + let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + workspace.setPanelPinned(panelId: firstPanelId, pinned: true) + UserDefaults.standard.set( + NewSurfacePlacement.top.rawValue, + forKey: SurfacePlacementSettings.placementKey + ) + workspace.focusPanel(thirdPanel.id) + let createdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + + XCTAssertEqual( + orderedPanelIds(in: workspace, paneId: paneId), + [firstPanelId, createdPanel.id, secondPanel.id, thirdPanel.id] + ) + XCTAssertEqual(workspace.focusedPanelId, createdPanel.id) + } + } +} + @MainActor final class WorkspaceCreationPlacementTests: XCTestCase { diff --git a/web/app/[locale]/docs/configuration/page.tsx b/web/app/[locale]/docs/configuration/page.tsx index 22b9bd91915c..5976fc0970eb 100644 --- a/web/app/[locale]/docs/configuration/page.tsx +++ b/web/app/[locale]/docs/configuration/page.tsx @@ -56,6 +56,7 @@ const settingsFileExample = `{ // "appearance": "dark", // "menuBarOnly": false, // "newWorkspacePlacement": "afterCurrent", + // "newSurfacePlacement": "afterCurrent", // "iMessageMode": true // }, diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 6476c9081112..81a07971c4b7 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -228,6 +228,12 @@ "default": "afterCurrent", "description": "Where new workspaces are inserted in the sidebar." }, + "newSurfacePlacement": { + "type": "string", + "enum": ["top", "afterCurrent", "end"], + "default": "afterCurrent", + "description": "Where new surfaces are inserted in the current pane's tab strip." + }, "minimalMode": { "type": "boolean", "default": false, From 9eb7a0c31c036b7c66f859a40828337829369917 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 11 May 2026 21:00:46 -0700 Subject: [PATCH 2/8] Fix explicit surface placement ownership --- Sources/Workspace.swift | 53 +++++++++++++++++------------- cmuxTests/WorkspaceUnitTests.swift | 10 ++---- 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index ccafedf36a65..4cd9d644ea65 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -10019,6 +10019,7 @@ final class Workspace: Identifiable, ObservableObject { tmuxStartCommand: String? = nil, initialInput: String? = nil, startupEnvironment: [String: String] = [:], + targetIndex: Int? = nil, placementOverride: NewSurfacePlacement? = nil ) -> TerminalPanel? { let shouldFocusNewTab = focus ?? (bonsplitController.focusedPaneId == paneId) @@ -10079,12 +10080,16 @@ final class Workspace: Identifiable, ObservableObject { } surfaceIdToPanelId[newTabId] = newPanel.id - applyNewSurfacePlacement( - to: newTabId, - inPane: paneId, - selectionBeforeCreation: selectionBeforeCreation, - placementOverride: placementOverride - ) + if let targetIndex { + _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) + } else { + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: placementOverride + ) + } publishCmuxSurfaceCreated(newPanel.id, paneId: paneId, kind: "terminal", origin: "terminal_tab", focused: shouldFocusNewTab) // bonsplit's createTab may not reliably emit didSelectTab, and its internal selection @@ -10231,6 +10236,7 @@ final class Workspace: Identifiable, ObservableObject { preferredProfileID: UUID? = nil, bypassInsecureHTTPHostOnce: String? = nil, creationPolicy: BrowserPanelCreationPolicy = .userInitiated, + targetIndex: Int? = nil, placementOverride: NewSurfacePlacement? = nil ) -> BrowserPanel? { let browserEnabled = BrowserAvailabilitySettings.isEnabled() @@ -10280,12 +10286,16 @@ final class Workspace: Identifiable, ObservableObject { surfaceIdToPanelId[newTabId] = browserPanel.id setPreferredBrowserProfileID(browserPanel.profileID) - applyNewSurfacePlacement( - to: newTabId, - inPane: paneId, - selectionBeforeCreation: selectionBeforeCreation, - placementOverride: insertAtEnd ? .end : placementOverride - ) + if let targetIndex { + _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) + } else { + applyNewSurfacePlacement( + to: newTabId, + inPane: paneId, + selectionBeforeCreation: selectionBeforeCreation, + placementOverride: insertAtEnd ? .end : placementOverride + ) + } publishCmuxSurfaceCreated(browserPanel.id, paneId: paneId, kind: "browser", origin: "browser_tab", focused: shouldFocusNewTab) // Match terminal behavior: enforce deterministic selection + focus. @@ -12467,8 +12477,7 @@ final class Workspace: Identifiable, ObservableObject { private func createTerminalToRight(of anchorTabId: TabID, inPane paneId: PaneID) { let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - guard let newPanel = newTerminalSurface(inPane: paneId, focus: true) else { return } - _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) + _ = newTerminalSurface(inPane: paneId, focus: true, targetIndex: targetIndex) } private func createTerminalFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID) { @@ -12482,13 +12491,13 @@ final class Workspace: Identifiable, ObservableObject { private func createBrowserToRight(of anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) let preferredProfileID = panelIdFromSurfaceId(anchorTabId).flatMap { browserPanel(for: $0)?.profileID } - guard let newPanel = newBrowserSurface( + _ = newBrowserSurface( inPane: paneId, url: url, focus: true, - preferredProfileID: preferredProfileID - ) else { return } - _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) + preferredProfileID: preferredProfileID, + targetIndex: targetIndex + ) } private func createBrowserFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { @@ -12503,13 +12512,13 @@ final class Workspace: Identifiable, ObservableObject { guard let panelId = panelIdFromSurfaceId(anchorTabId), let browser = browserPanel(for: panelId) else { return } let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - guard let newPanel = newBrowserSurface( + _ = newBrowserSurface( inPane: paneId, url: browser.currentURL, focus: true, - preferredProfileID: browser.profileID - ) else { return } - _ = reorderSurface(panelId: newPanel.id, toIndex: targetIndex) + preferredProfileID: browser.profileID, + targetIndex: targetIndex + ) } private func promptRenamePanel(tabId: TabID) { diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 2433adf4f41a..cfe1971faa5f 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -2383,18 +2383,14 @@ final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { } func testTopPlacementInsertsUnpinnedSurfaceAfterPinnedSurfaces() throws { - try withSurfacePlacement(.end) { + try withSurfacePlacement(.top) { let workspace = Workspace() let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) - let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) - let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) + let secondPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true, placementOverride: .end)) + let thirdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true, placementOverride: .end)) workspace.setPanelPinned(panelId: firstPanelId, pinned: true) - UserDefaults.standard.set( - NewSurfacePlacement.top.rawValue, - forKey: SurfacePlacementSettings.placementKey - ) workspace.focusPanel(thirdPanel.id) let createdPanel = try XCTUnwrap(workspace.newTerminalSurface(inPane: paneId, focus: true)) From 2886d84bcff0f87b5dab28ce342d4d3fbfc15e5b Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 02:31:09 -0700 Subject: [PATCH 3/8] Fix context browser profile placement --- Sources/Workspace.swift | 13 ++++++-- cmuxTests/WorkspaceUnitTests.swift | 52 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 6104b1f7f056..3b61a6afbf6c 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -12566,7 +12566,7 @@ final class Workspace: Identifiable, ObservableObject { private func createBrowserToRight(of anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - let preferredProfileID = panelIdFromSurfaceId(anchorTabId).flatMap { browserPanel(for: $0)?.profileID } + let preferredProfileID = browserProfileID(forSurface: anchorTabId) _ = newBrowserSurface( inPane: paneId, url: url, @@ -12576,12 +12576,21 @@ final class Workspace: Identifiable, ObservableObject { ) } + private func browserProfileID(forSurface surfaceId: TabID) -> UUID? { + panelIdFromSurfaceId(surfaceId).flatMap { browserPanel(for: $0)?.profileID } + } + private func createBrowserFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { guard SurfacePlacementSettings.current() != .afterCurrent else { createBrowserToRight(of: anchorTabId, inPane: paneId, url: url) return } - _ = newBrowserSurface(inPane: paneId, url: url, focus: true) + _ = newBrowserSurface( + inPane: paneId, + url: url, + focus: true, + preferredProfileID: browserProfileID(forSurface: anchorTabId) + ) } private func duplicateBrowserToRight(anchorTabId: TabID, inPane paneId: PaneID) { diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index cfe1971faa5f..42606f90e33c 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -2363,6 +2363,58 @@ final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { } } + func testContextMenuNewBrowserUsesAnchorProfileWithEndPlacement() throws { + try withSurfacePlacement(.end) { + let workspace = Workspace() + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let initialPanelId = try XCTUnwrap(workspace.focusedPanelId) + let anchorProfile = try makeTemporaryBrowserProfile(named: "ContextMenuAnchor") + let selectedProfile = try makeTemporaryBrowserProfile(named: "ContextMenuSelected") + let anchorBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: anchorProfile.id, + placementOverride: .end + ) + ) + let selectedBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: selectedProfile.id, + placementOverride: .end + ) + ) + let anchorTabId = try XCTUnwrap(workspace.surfaceIdFromPanelId(anchorBrowser.id)) + let anchorTab = try XCTUnwrap(workspace.bonsplitController.tab(anchorTabId)) + + workspace.focusPanel(selectedBrowser.id) + workspace.splitTabBar( + workspace.bonsplitController, + didRequestTabContextAction: .newBrowserToRight, + for: anchorTab, + inPane: paneId + ) + + let order = orderedPanelIds(in: workspace, paneId: paneId) + let createdPanelId = try XCTUnwrap( + order.first { ![initialPanelId, anchorBrowser.id, selectedBrowser.id].contains($0) } + ) + let createdBrowser = try XCTUnwrap(workspace.browserPanel(for: createdPanelId)) + XCTAssertEqual( + order, + [initialPanelId, anchorBrowser.id, selectedBrowser.id, createdPanelId] + ) + XCTAssertEqual( + createdBrowser.profileID, + anchorProfile.id, + "Expected context-menu browser creation to inherit the right-clicked tab profile, not the selected tab profile" + ) + XCTAssertEqual(workspace.focusedPanelId, createdPanelId) + } + } + func testDefaultPlacementInsertsNewTerminalSurfaceAfterFocusedSurface() throws { try withSurfacePlacement(.afterCurrent) { let workspace = Workspace() From e7993e5a8ee300b683eb501ea4b28411798f344d Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 13:22:06 -0700 Subject: [PATCH 4/8] Fix socket surface placement ownership --- Sources/TerminalController.swift | 45 ++++++++++++++------------------ Sources/Workspace.swift | 2 +- 2 files changed, 21 insertions(+), 26 deletions(-) diff --git a/Sources/TerminalController.swift b/Sources/TerminalController.swift index 4f2a966c7a22..669e93d6cd0c 100644 --- a/Sources/TerminalController.swift +++ b/Sources/TerminalController.swift @@ -5640,20 +5640,6 @@ class TerminalController { result = .ok(payload) } - @MainActor - func insertionIndexToRight(anchorTabId: TabID, inPane paneId: PaneID) -> Int { - let tabs = workspace.bonsplitController.tabs(inPane: paneId) - guard let anchorIndex = tabs.firstIndex(where: { $0.id == anchorTabId }) else { return tabs.count } - let pinnedCount = tabs.reduce(into: 0) { count, tab in - if let panelId = workspace.panelIdFromSurfaceId(tab.id), - workspace.isPanelPinned(panelId) { - count += 1 - } - } - let rawTarget = min(anchorIndex + 1, tabs.count) - return max(rawTarget, pinnedCount) - } - @MainActor func closeTabs(_ tabIds: [TabID]) -> (closed: Int, skippedPinned: Int) { var closed = 0 @@ -5730,16 +5716,17 @@ class TerminalController { return } - let targetIndex = insertionIndexToRight(anchorTabId: anchorTabId, inPane: paneId) + let targetIndex = workspace.insertionIndexToRight(of: anchorTabId, inPane: paneId) guard let newPanel = workspace.newBrowserSurface( inPane: paneId, url: browserPanel.currentURL, - focus: focus + focus: focus, + preferredProfileID: browserPanel.profileID, + targetIndex: targetIndex ) else { result = .err(code: "internal_error", message: "Failed to duplicate tab", data: nil) return } - _ = workspace.reorderSurface(panelId: newPanel.id, toIndex: targetIndex, focus: focus) finish([ "created_surface_id": newPanel.id.uuidString, "created_surface_ref": v2Ref(kind: .surface, uuid: newPanel.id), @@ -5754,12 +5741,15 @@ class TerminalController { return } - let targetIndex = insertionIndexToRight(anchorTabId: anchorTabId, inPane: paneId) - guard let newPanel = workspace.newTerminalSurface(inPane: paneId, focus: focus) else { + let targetIndex = workspace.insertionIndexToRight(of: anchorTabId, inPane: paneId) + guard let newPanel = workspace.newTerminalSurface( + inPane: paneId, + focus: focus, + targetIndex: targetIndex + ) else { result = .err(code: "internal_error", message: "Failed to create tab", data: nil) return } - _ = workspace.reorderSurface(panelId: newPanel.id, toIndex: targetIndex, focus: focus) finish([ "created_surface_id": newPanel.id.uuidString, "created_surface_ref": v2Ref(kind: .surface, uuid: newPanel.id), @@ -5789,12 +5779,17 @@ class TerminalController { return } - let targetIndex = insertionIndexToRight(anchorTabId: anchorTabId, inPane: paneId) - guard let newPanel = workspace.newBrowserSurface(inPane: paneId, url: url, focus: focus) else { + let targetIndex = workspace.insertionIndexToRight(of: anchorTabId, inPane: paneId) + guard let newPanel = workspace.newBrowserSurface( + inPane: paneId, + url: url, + focus: focus, + preferredProfileID: workspace.browserPanel(for: surfaceId)?.profileID, + targetIndex: targetIndex + ) else { result = .err(code: "internal_error", message: "Failed to create tab", data: nil) return } - _ = workspace.reorderSurface(panelId: newPanel.id, toIndex: targetIndex, focus: focus) finish([ "created_surface_id": newPanel.id.uuidString, "created_surface_ref": v2Ref(kind: .surface, uuid: newPanel.id), @@ -7655,14 +7650,14 @@ class TerminalController { var sourcePlaceholder: UUID? var targetPlaceholder: UUID? if workspace.bonsplitController.tabs(inPane: sourcePane).count <= 1 { - sourcePlaceholder = workspace.newTerminalSurface(inPane: sourcePane, focus: false)?.id + sourcePlaceholder = workspace.newTerminalSurface(inPane: sourcePane, focus: false, placementOverride: .end)?.id if sourcePlaceholder == nil { result = .err(code: "internal_error", message: "Failed to create source placeholder surface", data: nil) return } } if workspace.bonsplitController.tabs(inPane: targetPane).count <= 1 { - targetPlaceholder = workspace.newTerminalSurface(inPane: targetPane, focus: false)?.id + targetPlaceholder = workspace.newTerminalSurface(inPane: targetPane, focus: false, placementOverride: .end)?.id if targetPlaceholder == nil { result = .err(code: "internal_error", message: "Failed to create target placeholder surface", data: nil) return diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 3b61a6afbf6c..621594f41bdd 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -8333,7 +8333,7 @@ final class Workspace: Identifiable, ObservableObject { _ = bonsplitController.reorderTab(newTabId, toIndex: targetIndex) } - private func insertionIndexToRight(of anchorTabId: TabID, inPane paneId: PaneID) -> Int { + func insertionIndexToRight(of anchorTabId: TabID, inPane paneId: PaneID) -> Int { let tabs = bonsplitController.tabs(inPane: paneId) guard let anchorIndex = tabs.firstIndex(where: { $0.id == anchorTabId }) else { return tabs.count } let pinnedCount = tabs.reduce(into: 0) { count, tab in From adb754aa331d08c2eada59ffb626b8fe60dcbc58 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 13:23:35 -0700 Subject: [PATCH 5/8] Add socket surface placement coverage --- cmuxTests/WorkspaceUnitTests.swift | 152 +++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index 42606f90e33c..ee975f736de1 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -2318,6 +2318,46 @@ final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { .compactMap { workspace.panelIdFromSurfaceId($0.id) } } + private func v2Result( + method: String, + params: [String: Any], + file: StaticString = #filePath, + line: UInt = #line + ) throws -> [String: Any] { + let request: [String: Any] = [ + "id": method, + "method": method, + "params": params + ] + let requestData = try JSONSerialization.data(withJSONObject: request) + let requestLine = try XCTUnwrap(String(data: requestData, encoding: .utf8), file: file, line: line) + let raw = TerminalController.shared.handleSocketLine(requestLine) + let responseData = try XCTUnwrap(raw.data(using: .utf8), file: file, line: line) + let envelope = try XCTUnwrap( + JSONSerialization.jsonObject(with: responseData) as? [String: Any], + raw, + file: file, + line: line + ) + XCTAssertEqual(envelope["ok"] as? Bool, true, raw, file: file, line: line) + return try XCTUnwrap(envelope["result"] as? [String: Any], raw, file: file, line: line) + } + + private func withBrowserEnabled(run body: () throws -> Void) rethrows { + let defaults = UserDefaults.standard + let key = BrowserAvailabilitySettings.disabledKey + let previousValue = defaults.object(forKey: key) + defaults.set(false, forKey: key) + defer { + if let previousValue { + defaults.set(previousValue, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + } + try body() + } + func testEndPlacementAppendsNewTerminalSurfaceToPaneEnd() throws { try withSurfacePlacement(.end) { let workspace = Workspace() @@ -2415,6 +2455,118 @@ final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { } } + func testSocketNewBrowserRightUsesAnchorProfileWithEndPlacement() throws { + try withBrowserEnabled { + try withSurfacePlacement(.end) { + let manager = TabManager() + TerminalController.shared.setActiveTabManager(manager) + defer { TerminalController.shared.setActiveTabManager(nil) } + + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let initialPanelId = try XCTUnwrap(workspace.focusedPanelId) + let anchorProfile = try makeTemporaryBrowserProfile(named: "SocketAnchor") + let selectedProfile = try makeTemporaryBrowserProfile(named: "SocketSelected") + let anchorBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: anchorProfile.id, + placementOverride: .end + ) + ) + let selectedBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: selectedProfile.id, + placementOverride: .end + ) + ) + + workspace.focusPanel(selectedBrowser.id) + let result = try v2Result( + method: "surface.action", + params: [ + "workspace_id": workspace.id.uuidString, + "surface_id": anchorBrowser.id.uuidString, + "action": "new_browser_right", + "focus": true + ] + ) + + let createdIDRaw = try XCTUnwrap(result["created_surface_id"] as? String) + let createdID = try XCTUnwrap(UUID(uuidString: createdIDRaw)) + let createdBrowser = try XCTUnwrap(workspace.browserPanel(for: createdID)) + XCTAssertEqual( + orderedPanelIds(in: workspace, paneId: paneId), + [initialPanelId, anchorBrowser.id, createdID, selectedBrowser.id] + ) + XCTAssertEqual( + createdBrowser.profileID, + anchorProfile.id, + "Expected socket new-browser-right to inherit the anchor surface profile, not the selected surface profile" + ) + } + } + } + + func testSocketDuplicateBrowserUsesDuplicatedProfileWithEndPlacement() throws { + try withBrowserEnabled { + try withSurfacePlacement(.end) { + let manager = TabManager() + TerminalController.shared.setActiveTabManager(manager) + defer { TerminalController.shared.setActiveTabManager(nil) } + + let workspace = try XCTUnwrap(manager.selectedWorkspace) + let paneId = try XCTUnwrap(workspace.bonsplitController.focusedPaneId) + let initialPanelId = try XCTUnwrap(workspace.focusedPanelId) + let anchorProfile = try makeTemporaryBrowserProfile(named: "DuplicateAnchor") + let selectedProfile = try makeTemporaryBrowserProfile(named: "DuplicateSelected") + let anchorBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: anchorProfile.id, + placementOverride: .end + ) + ) + let selectedBrowser = try XCTUnwrap( + workspace.newBrowserSurface( + inPane: paneId, + focus: true, + preferredProfileID: selectedProfile.id, + placementOverride: .end + ) + ) + + workspace.focusPanel(selectedBrowser.id) + let result = try v2Result( + method: "surface.action", + params: [ + "workspace_id": workspace.id.uuidString, + "surface_id": anchorBrowser.id.uuidString, + "action": "duplicate", + "focus": true + ] + ) + + let createdIDRaw = try XCTUnwrap(result["created_surface_id"] as? String) + let createdID = try XCTUnwrap(UUID(uuidString: createdIDRaw)) + let createdBrowser = try XCTUnwrap(workspace.browserPanel(for: createdID)) + XCTAssertEqual( + orderedPanelIds(in: workspace, paneId: paneId), + [initialPanelId, anchorBrowser.id, createdID, selectedBrowser.id] + ) + XCTAssertEqual( + createdBrowser.profileID, + anchorProfile.id, + "Expected socket duplicate to preserve the duplicated browser profile" + ) + } + } + } + func testDefaultPlacementInsertsNewTerminalSurfaceAfterFocusedSurface() throws { try withSurfacePlacement(.afterCurrent) { let workspace = Workspace() From 2fe8b9dae4b0749fa7aa97aa9fa3c296114eae7a Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Tue, 12 May 2026 13:24:57 -0700 Subject: [PATCH 6/8] Fix socket placement test decoding --- cmuxTests/WorkspaceUnitTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmuxTests/WorkspaceUnitTests.swift b/cmuxTests/WorkspaceUnitTests.swift index ee975f736de1..d9cc5e4d7a63 100644 --- a/cmuxTests/WorkspaceUnitTests.swift +++ b/cmuxTests/WorkspaceUnitTests.swift @@ -2334,7 +2334,7 @@ final class WorkspaceSurfaceCreationPlacementTests: XCTestCase { let raw = TerminalController.shared.handleSocketLine(requestLine) let responseData = try XCTUnwrap(raw.data(using: .utf8), file: file, line: line) let envelope = try XCTUnwrap( - JSONSerialization.jsonObject(with: responseData) as? [String: Any], + try JSONSerialization.jsonObject(with: responseData) as? [String: Any], raw, file: file, line: line From ae717e811429576419196fb7c224e833bb2b843c Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Sun, 17 May 2026 23:20:17 -0700 Subject: [PATCH 7/8] fix: clarify context-menu surface placement --- Sources/Workspace.swift | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index da28aa48f84c..4dc8f70070c0 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -13082,28 +13082,19 @@ final class Workspace: Identifiable, ObservableObject { .filter { $0 != anchorTabId } } - private func createTerminalToRight(of anchorTabId: TabID, inPane paneId: PaneID) { - let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - _ = newTerminalSurface(inPane: paneId, focus: true, targetIndex: targetIndex) + private func contextMenuSurfaceTargetIndex(anchorTabId: TabID, inPane paneId: PaneID) -> Int? { + // Bonsplit's action names say "to right" because that was the original + // after-current behavior. User-created context-menu surfaces still honor + // app.newSurfacePlacement; only afterCurrent needs an anchor-relative index. + guard SurfacePlacementSettings.current() == .afterCurrent else { return nil } + return insertionIndexToRight(of: anchorTabId, inPane: paneId) } private func createTerminalFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID) { - guard SurfacePlacementSettings.current() != .afterCurrent else { - createTerminalToRight(of: anchorTabId, inPane: paneId) - return - } - _ = newTerminalSurface(inPane: paneId, focus: true) - } - - private func createBrowserToRight(of anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { - let targetIndex = insertionIndexToRight(of: anchorTabId, inPane: paneId) - let preferredProfileID = browserProfileID(forSurface: anchorTabId) - _ = newBrowserSurface( + _ = newTerminalSurface( inPane: paneId, - url: url, focus: true, - preferredProfileID: preferredProfileID, - targetIndex: targetIndex + targetIndex: contextMenuSurfaceTargetIndex(anchorTabId: anchorTabId, inPane: paneId) ) } @@ -13112,15 +13103,12 @@ final class Workspace: Identifiable, ObservableObject { } private func createBrowserFromContextMenu(anchorTabId: TabID, inPane paneId: PaneID, url: URL? = nil) { - guard SurfacePlacementSettings.current() != .afterCurrent else { - createBrowserToRight(of: anchorTabId, inPane: paneId, url: url) - return - } _ = newBrowserSurface( inPane: paneId, url: url, focus: true, - preferredProfileID: browserProfileID(forSurface: anchorTabId) + preferredProfileID: browserProfileID(forSurface: anchorTabId), + targetIndex: contextMenuSurfaceTargetIndex(anchorTabId: anchorTabId, inPane: paneId) ) } From eaef6e600ef42ff40e2a046e3f89dbdb24776ff5 Mon Sep 17 00:00:00 2001 From: austinpower1258 Date: Mon, 18 May 2026 22:00:09 -0700 Subject: [PATCH 8/8] fix: localize surface placement strings --- Resources/Localizable.xcstrings | 844 +++++++++++++++++++++++++++++++- 1 file changed, 830 insertions(+), 14 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index e1cbf4665b51..7c15a9fecfaa 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -53152,6 +53152,108 @@ "state": "translated", "value": "新規サーフェスの配置" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "新 Surface 位置" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "新增 Surface 位置" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 화면 위치" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Platzierung neuer Oberflächen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Ubicación de nueva superficie" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Emplacement des nouvelles surfaces" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Posizionamento nuova superficie" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Placering af ny overflade" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Umieszczenie nowej powierzchni" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Расположение новой поверхности" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Pozicija nove površine" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "موضع السطح الجديد" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Plassering av ny flate" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Posição da Nova Superfície" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ตำแหน่งพื้นผิวใหม่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni Yüzey Konumu" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Розташування нової поверхні" + } } } }, @@ -72934,21 +73036,123 @@ } }, "settings.search.alias.setting.app.new-surface-placement": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "app.newSurfacePlacement surface tab insert position order start end after current" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "app.newSurfacePlacement surface tab insert position order start end after current 新規サーフェス 新しいタブ 挿入位置 並び順 先頭 末尾 現在の後" - } - } + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current 新規サーフェス 新しいタブ 挿入位置 並び順 先頭 末尾 現在の後" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current 应用 通用 偏好设置 Surface 标签页 插入 位置 顺序 开头 末尾 当前之后 新 Surface 位置" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current 應用程式 一般 偏好設定 Surface 分頁 插入 位置 順序 開頭 末尾 目前之後 新增 Surface 位置" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current 앱 일반 환경설정 화면 탭 삽입 위치 순서 시작 끝 현재 다음 새 화면 위치" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current app allgemein einstellungen Oberfläche Tab einfügen Position Reihenfolge Anfang Ende nach aktuellem Platzierung neuer Oberflächen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current aplicación general preferencias superficie pestaña insertar posición orden inicio final después del actual ubicación de nueva superficie" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current application général préférences surface onglet insertion position ordre début fin après l'actuel emplacement des nouvelles surfaces" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current app generale preferenze superficie scheda inserisci posizione ordine inizio fine dopo la corrente posizionamento nuova superficie" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current app generelt indstillinger overflade fane indsæt placering rækkefølge start sidst efter nuværende placering af ny overflade" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current aplikacja ogólne preferencje powierzchnia karta wstaw pozycja kolejność początek koniec po bieżącej umieszczenie nowej powierzchni" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current приложение общие настройки поверхность вкладка вставить позиция порядок начало конец после текущего расположение новой поверхности" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current aplikacija opće postavke površina kartica umetni pozicija redoslijed početak kraj poslije trenutnog pozicija nove površine" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current تطبيق عام تفضيلات سطح تبويب إدراج موضع ترتيب بداية نهاية بعد الحالي موضع السطح الجديد" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current app generelt innstillinger flate fane sett inn plassering rekkefølge start slutt etter gjeldende plassering av ny flate" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current app geral preferências superfície aba inserir posição ordem início final após a atual posição da nova superfície" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current แอป ทั่วไป ค่ากำหนด พื้นผิว แท็บ แทรก ตำแหน่ง ลำดับ เริ่มต้น ท้ายสุด หลังรายการปัจจุบัน ตำแหน่งพื้นผิวใหม่" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current uygulama genel tercihler yüzey sekme ekle konum sıra başlangıç son geçerli sonrasına yeni yüzey konumu" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "app.newSurfacePlacement surface tab insert position order start end after current програма загальні параметри поверхня вкладка вставити позиція порядок початок кінець після поточної розташування нової поверхні" + } } + } }, "settings.search.alias.setting.app.workspace-inherit-working-directory": { "extractionState": "manual", @@ -101011,6 +101215,108 @@ "state": "translated", "value": "現在の後" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "当前之后" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "目前之後" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "현재 다음" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nach aktuellem" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Después del actual" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Après l'actuel" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Dopo la corrente" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Efter nuværende" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Po bieżącej" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "После текущего" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Poslije trenutnog" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "بعد الحالي" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Etter gjeldende" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Após a atual" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "หลังรายการปัจจุบัน" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Geçerli sonrasına" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Після поточної" + } } } }, @@ -101028,6 +101334,108 @@ "state": "translated", "value": "アクティブなサーフェスの直後に新しいサーフェスを挿入します。" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在活动 Surface 之后插入新 Surface。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "將新 Surface 插入目前使用中 Surface 的後方。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "활성 화면 바로 다음에 새 화면을 삽입합니다." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Oberflächen direkt nach der aktiven Oberfläche einfügen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Insertar nuevas superficies justo después de la superficie activa." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Insérer les nouvelles surfaces juste après la surface active." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inserisci le nuove superfici subito dopo quella attiva." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indsæt nye overflader direkte efter den aktive overflade." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wstaw nowe powierzchnie bezpośrednio po aktywnej." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Вставлять новые поверхности сразу после активной." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Umetnite nove površine odmah nakon aktivne površine." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إدراج الأسطح الجديدة مباشرة بعد السطح النشط." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Sett inn nye flater rett etter den aktive flaten." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Inserir novas superfícies diretamente após a superfície ativa." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แทรกพื้นผิวใหม่หลังพื้นผิวที่ใช้งานอยู่โดยตรง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni yüzeyleri etkin yüzeyin hemen sonrasına ekle." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вставляти нові поверхні одразу після активної." + } } } }, @@ -101045,6 +101453,108 @@ "state": "translated", "value": "末尾" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "末尾" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "底部" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "끝" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Ende" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Final" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Fin" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Fine" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Sidst" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Na końcu" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "В конец" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Kraj" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "النهاية" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Slutt" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Final" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ท้ายสุด" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sona" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "В кінці" + } } } }, @@ -101062,6 +101572,108 @@ "state": "translated", "value": "新しいサーフェスをペインのタブ列の末尾に追加します。" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "将新 Surface 添加到窗格标签栏末尾。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "將新 Surface 附加到窗格分頁列末尾。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 화면을 창의 탭 표시줄 끝에 추가합니다." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Oberflächen am Ende der Tab-Leiste des Bereichs anfügen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Agregar nuevas superficies al final de la barra de pestañas del panel." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajouter les nouvelles surfaces à la fin de la barre d'onglets du panneau." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Aggiungi le nuove superfici alla fine della barra delle schede del riquadro." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Tilføj nye overflader sidst på rudens fanelinje." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Dodaj nowe powierzchnie na końcu paska kart panelu." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Добавлять новые поверхности в конец панели вкладок области." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Dodajte nove površine na kraj trake kartica panela." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إلحاق الأسطح الجديدة بنهاية شريط تبويبات اللوحة." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Legg til nye flater på slutten av faneraden i ruten." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Adicionar novas superfícies ao final da barra de abas do painel." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เพิ่มพื้นผิวใหม่ที่ท้ายแถบแท็บของบานหน้าต่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni yüzeyleri bölmenin sekme çubuğunun sonuna ekle." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Додавати нові поверхні в кінець панелі вкладок області." + } } } }, @@ -101079,6 +101691,108 @@ "state": "translated", "value": "先頭" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "开头" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "開頭" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "시작" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Anfang" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Inicio" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Début" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inizio" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Początek" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "В начало" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Početak" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "البداية" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Start" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Início" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "เริ่มต้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Başlangıç" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "На початку" + } } } }, @@ -101096,6 +101810,108 @@ "state": "translated", "value": "新しいサーフェスをペインのタブ列の先頭に挿入します。" } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在窗格标签栏开头插入新 Surface。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "將新 Surface 插入窗格分頁列開頭。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "새 화면을 창의 탭 표시줄 시작 부분에 삽입합니다." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Neue Oberflächen am Anfang der Tab-Leiste des Bereichs einfügen." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Insertar nuevas superficies al inicio de la barra de pestañas del panel." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Insérer les nouvelles surfaces au début de la barre d'onglets du panneau." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Inserisci le nuove superfici all'inizio della barra delle schede del riquadro." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Indsæt nye overflader først på rudens fanelinje." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Wstaw nowe powierzchnie na początku paska kart panelu." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Вставлять новые поверхности в начало панели вкладок области." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Umetnite nove površine na početak trake kartica panela." + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إدراج الأسطح الجديدة في بداية شريط تبويبات اللوحة." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Sett inn nye flater ved starten av faneraden i ruten." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Inserir novas superfícies no início da barra de abas do painel." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "แทรกพื้นผิวใหม่ที่จุดเริ่มต้นของแถบแท็บของบานหน้าต่าง" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Yeni yüzeyleri bölmenin sekme çubuğunun başına ekle." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Вставляти нові поверхні на початок панелі вкладок області." + } } } },