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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -50081,6 +50081,40 @@
}
}
},
"command.newWorkspaceAbove.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "New Workspace Above"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上に新規ワークスペース"
}
}
}
},
"command.newWorkspaceBelow.title": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "New Workspace Below"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "下に新規ワークスペース"
}
}
}
},
"command.nextTabInPane.subtitle": {
"extractionState": "manual",
"localizations": {
Comment on lines 50082 to 50120

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Missing translations for 18 locales

The catalog already contains 20 locales — ar, bs, da, de, es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant in addition to en and ja — but all four new keys (command.newWorkspaceAbove.title, command.newWorkspaceBelow.title, contextMenu.newWorkspaceAbove, contextMenu.newWorkspaceBelow) only supply en and ja entries. Every other supported locale will fall back to the en default value at runtime, leaving users of the other 18 locales with untranslated strings.

Rule Used: Flag production user-facing text that is not fully... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Expand Down Expand Up @@ -64002,6 +64036,40 @@
}
}
},
"contextMenu.newWorkspaceAbove": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "New Workspace Above"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "上に新規ワークスペース"
}
}
}
},
"contextMenu.newWorkspaceBelow": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "New Workspace Below"
}
},
"ja": {
"stringUnit": {
"state": "translated",
"value": "下に新規ワークスペース"
}
}
}
},
"contextMenu.newWindow": {
"extractionState": "manual",
"localizations": {
Expand Down
50 changes: 50 additions & 0 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6746,6 +6746,22 @@ struct ContentView: View {
keywords: ["create", "new", "workspace"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newWorkspaceAbove",
title: constant(String(localized: "command.newWorkspaceAbove.title", defaultValue: "New Workspace Above")),
subtitle: constant(String(localized: "command.newWorkspace.subtitle", defaultValue: "Workspace")),
keywords: ["create", "new", "workspace", "above", "before"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newWorkspaceBelow",
title: constant(String(localized: "command.newWorkspaceBelow.title", defaultValue: "New Workspace Below")),
subtitle: constant(String(localized: "command.newWorkspace.subtitle", defaultValue: "Workspace")),
keywords: ["create", "new", "workspace", "below", "after"]
)
)
contributions.append(
CommandPaletteCommandContribution(
commandId: "palette.newWindow",
Expand Down Expand Up @@ -7952,6 +7968,26 @@ struct ContentView: View {
debugSource: "palette.newWorkspace"
)
}
registry.register(commandId: "palette.newWorkspaceAbove") {
guard let selectedTabId = tabManager.selectedTabId else {
NSSound.beep()
return
}
guard tabManager.createWorkspaceAdjacent(to: selectedTabId, position: .above) != nil else {
NSSound.beep()
return
}
}
registry.register(commandId: "palette.newWorkspaceBelow") {
guard let selectedTabId = tabManager.selectedTabId else {
NSSound.beep()
return
}
guard tabManager.createWorkspaceAdjacent(to: selectedTabId, position: .below) != nil else {
NSSound.beep()
return
}
}
registry.register(commandId: "palette.openFolder") {
// Defer so the command palette dismisses before the modal sheet appears.
DispatchQueue.main.async {
Expand Down Expand Up @@ -16024,6 +16060,20 @@ struct TabItemView: View, Equatable {
}
.disabled(contextMenuPinState == nil)

Button(String(localized: "contextMenu.newWorkspaceAbove", defaultValue: "New Workspace Above")) {
guard tabManager.createWorkspaceAdjacent(to: tab.id, position: .above) != nil else {
NSSound.beep()
return
}
}

Button(String(localized: "contextMenu.newWorkspaceBelow", defaultValue: "New Workspace Below")) {
guard tabManager.createWorkspaceAdjacent(to: tab.id, position: .below) != nil else {
NSSound.beep()
return
}
}

workspaceGroupContextMenuSection(targetIds: targetIds, isMulti: isMulti)

if let key = renameWorkspaceShortcut.keyEquivalent {
Expand Down
17 changes: 17 additions & 0 deletions Sources/SidebarWorkspaceGroupHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
let onToggleCollapsed: () -> Void
let onFocusAnchor: () -> Void
let onTapPlus: () -> Void
let onNewWorkspaceAbove: () -> Void
let onNewWorkspaceBelow: () -> Void
let onRunResolvedItem: (CmuxResolvedConfigMenuAction) -> Void
let onRename: () -> Void
let onTogglePinned: () -> Void
Expand Down Expand Up @@ -241,6 +243,21 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
isHovered = hovering
}
.contextMenu {
Button(
String(
localized: "contextMenu.newWorkspaceAbove",
defaultValue: "New Workspace Above"
),
action: onNewWorkspaceAbove
)
Button(
String(
localized: "contextMenu.newWorkspaceBelow",
defaultValue: "New Workspace Below"
),
action: onNewWorkspaceBelow
)
Divider()
Button(
String(
localized: "workspaceGroup.contextMenu.rename",
Expand Down
124 changes: 123 additions & 1 deletion Sources/TabManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ enum NewWorkspacePlacement: String, CaseIterable, Identifiable {
}
}

enum WorkspaceAdjacentInsertionPosition: Sendable {
case above
case below
}

enum WorkspaceAutoReorderSettings {
static let key = "workspaceAutoReorderOnNotification"
static let defaultValue = true
Expand Down Expand Up @@ -2612,11 +2617,12 @@ class TabManager: ObservableObject {
select: Bool = true,
eagerLoadTerminal: Bool = false,
placementOverride: NewWorkspacePlacement? = nil,
sourceWorkspaceOverride: Workspace? = nil,
autoWelcomeIfNeeded: Bool = true,
autoRefreshMetadata: Bool = true,
normalizeWorkspaceGroupsAfterInsert: Bool = true
) -> Workspace {
let sourceWorkspace = selectedWorkspace
let sourceWorkspace = sourceWorkspaceOverride ?? selectedWorkspace
let capturedTabs = tabs
// Snapshot the selected tab from the pinned workspace instead of rereading the
// @Published selectedTabId storage after the inheritance helpers. The arm64 Nightly
Expand Down Expand Up @@ -4273,6 +4279,122 @@ class TabManager: ObservableObject {
return newWorkspace
}

@discardableResult
func createWorkspaceAdjacent(
to referenceWorkspaceId: UUID,
position: WorkspaceAdjacentInsertionPosition,
select: Bool = true
) -> Workspace? {
guard let referenceWorkspace = tabs.first(where: { $0.id == referenceWorkspaceId }) else {
return nil
}

if let groupId = referenceWorkspace.groupId,
let group = workspaceGroups.first(where: { $0.id == groupId }) {
if group.anchorWorkspaceId == referenceWorkspaceId {
return createWorkspaceAdjacentToGroup(
groupId: groupId,
position: position,
select: select
)
}
return createWorkspaceAdjacentWithinGroup(
to: referenceWorkspace,
groupId: groupId,
position: position,
select: select
)
}

let newWorkspace = addWorkspace(
select: select,
placementOverride: .end,
sourceWorkspaceOverride: referenceWorkspace
)
reorderCreatedWorkspace(
newWorkspace.id,
adjacentTo: referenceWorkspaceId,
position: position
)
Comment on lines +4309 to +4318

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve pin tier for adjacent insertion targets.

Line 4314 / Line 4339 / Line 4366 reorder a newly created workspace that is always unpinned. Because reorderWorkspace clamps by pin tier, “above/below” against pinned references (or pinned groups/pinned group members) can land non-adjacent.

Suggested fix
@@
     func createWorkspaceAdjacent(
         to referenceWorkspaceId: UUID,
         position: WorkspaceAdjacentInsertionPosition,
         select: Bool = true
     ) -> Workspace? {
@@
         let newWorkspace = addWorkspace(
             select: select,
             placementOverride: .end,
             sourceWorkspaceOverride: referenceWorkspace
         )
+        newWorkspace.isPinned = referenceWorkspace.isPinned
         reorderCreatedWorkspace(
             newWorkspace.id,
             adjacentTo: referenceWorkspaceId,
             position: position
         )
         return newWorkspace
     }
@@
     private func createWorkspaceAdjacentWithinGroup(
         to referenceWorkspace: Workspace,
         groupId: UUID,
         position: WorkspaceAdjacentInsertionPosition,
         select: Bool
@@
         let newWorkspace = addWorkspace(
             workingDirectory: cwd,
             inheritWorkingDirectory: cwd == nil,
             select: select,
             placementOverride: .end,
             sourceWorkspaceOverride: referenceWorkspace,
             autoWelcomeIfNeeded: false
         )
+        newWorkspace.isPinned = referenceWorkspace.isPinned
         assignGroup(workspaceId: newWorkspace.id, groupId: groupId)
         reorderCreatedWorkspace(
             newWorkspace.id,
             adjacentTo: referenceWorkspace.id,
             position: position
         )
@@
     private func createWorkspaceAdjacentToGroup(
         groupId: UUID,
         position: WorkspaceAdjacentInsertionPosition,
         select: Bool
     ) -> Workspace? {
@@
         let newWorkspace = addWorkspace(
             select: select,
             placementOverride: .end,
             sourceWorkspaceOverride: anchorWorkspace
         )
+        newWorkspace.isPinned = group.isPinned
         let groupMemberIds = tabs.filter { $0.groupId == groupId && $0.id != newWorkspace.id }.map(\.id)
         switch position {

Also applies to: 4330-4343, 4357-4371

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/TabManager.swift` around lines 4309 - 4318, When creating then
immediately reordering a new workspace (via addWorkspace and
reorderCreatedWorkspace), the new workspace is always unpinned so
reorderWorkspace's pin-tier clamping can place it non-adjacent to a pinned
reference; modify the flow so the new workspace inherits the reference
workspace's pin tier (or pass an explicit pinTier/placementHint into
addWorkspace or reorderCreatedWorkspace) before calling
reorderWorkspace/reorderCreatedWorkspace so pin-tier clamping uses the correct
tier; update calls around addWorkspace/select: select, placementOverride: .end,
sourceWorkspaceOverride: referenceWorkspace and the subsequent
reorderCreatedWorkspace(newWorkspace.id, adjacentTo: referenceWorkspaceId,
position: position) (and the other similar blocks at the noted ranges) to
preserve the referenceWorkspace.pin state when reordering.

return newWorkspace
}

private func createWorkspaceAdjacentWithinGroup(
to referenceWorkspace: Workspace,
groupId: UUID,
position: WorkspaceAdjacentInsertionPosition,
select: Bool
) -> Workspace? {
guard let group = workspaceGroups.first(where: { $0.id == groupId }) else { return nil }
let cwd = tabs.first(where: { $0.id == group.anchorWorkspaceId })?.currentDirectory
let newWorkspace = addWorkspace(
workingDirectory: cwd,
inheritWorkingDirectory: cwd == nil,
Comment on lines +4329 to +4332

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Inherit the clicked grouped workspace directory

When this action is invoked from a workspace row inside a group whose current directory differs from the group anchor, the new workspace is created with the anchor's currentDirectory because workingDirectory: cwd is passed explicitly; that prevents sourceWorkspaceOverride: referenceWorkspace from supplying the clicked workspace's directory. The ungrouped adjacent path inherits from the reference workspace, so grouped rows behave inconsistently and “New Workspace Above/Below” can open in the wrong folder for mixed-directory groups.

Useful? React with 👍 / 👎.

select: select,
placementOverride: .end,
sourceWorkspaceOverride: referenceWorkspace,
autoWelcomeIfNeeded: false
)
assignGroup(workspaceId: newWorkspace.id, groupId: groupId)
reorderCreatedWorkspace(
newWorkspace.id,
adjacentTo: referenceWorkspace.id,
position: position
)
expandGroupIfNeeded(groupId: groupId, select: select)
return newWorkspace
}

private func createWorkspaceAdjacentToGroup(
groupId: UUID,
position: WorkspaceAdjacentInsertionPosition,
select: Bool
) -> Workspace? {
guard let group = workspaceGroups.first(where: { $0.id == groupId }),
let anchorWorkspace = tabs.first(where: { $0.id == group.anchorWorkspaceId }) else {
return nil
}
let newWorkspace = addWorkspace(
select: select,
placementOverride: .end,
sourceWorkspaceOverride: anchorWorkspace
)
let groupMemberIds = tabs.filter { $0.groupId == groupId && $0.id != newWorkspace.id }.map(\.id)
switch position {
case .above:
if let firstGroupWorkspaceId = groupMemberIds.first {
_ = reorderWorkspace(tabId: newWorkspace.id, before: firstGroupWorkspaceId)
}
case .below:
if let lastGroupWorkspaceId = groupMemberIds.last {
_ = reorderWorkspace(tabId: newWorkspace.id, after: lastGroupWorkspaceId)
}
}
return newWorkspace
}

private func reorderCreatedWorkspace(
_ workspaceId: UUID,
adjacentTo referenceWorkspaceId: UUID,
position: WorkspaceAdjacentInsertionPosition
) {
switch position {
case .above:
_ = reorderWorkspace(tabId: workspaceId, before: referenceWorkspaceId)
case .below:
_ = reorderWorkspace(tabId: workspaceId, after: referenceWorkspaceId)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pinned adjacent insert misplaces workspace

Medium Severity

Adjacent workspace creation appends via addWorkspace, then reorderCreatedWorkspace calls reorderWorkspace with before/after the reference. That path uses clampedReorderIndex, which keeps unpinned tabs out of the pinned prefix (and applies grouped pin bounds). For a pinned reference, “above” or “below” can clamp to the pin-tier edge instead of the slot next to that workspace, while the action still succeeds.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4e0f350. Configure here.


private func expandGroupIfNeeded(groupId: UUID, select: Bool) {
guard select,
let idx = workspaceGroups.firstIndex(where: { $0.id == groupId }),
workspaceGroups[idx].isCollapsed else {
return
}
workspaceGroups[idx].isCollapsed = false
}

/// Move an existing group member to the requested in-group slot. Called
/// after `createWorkspaceInGroup` and any other path that needs to
/// pin the new member relative to the group's members.
Expand Down
14 changes: 14 additions & 0 deletions Sources/VerticalTabsSidebar+WorkspaceGroups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ extension VerticalTabsSidebar {
let resolved = placement ?? WorkspaceGroupNewWorkspacePlacementSettings.resolved()
_ = tabManager.createWorkspaceInGroup(groupId: groupId, placement: resolved)
},
onNewWorkspaceAbove: { [weak tabManager, anchorId = group.anchorWorkspaceId] in
guard let tabManager,
tabManager.createWorkspaceAdjacent(to: anchorId, position: .above) != nil else {
NSSound.beep()
return
}
},
onNewWorkspaceBelow: { [weak tabManager, anchorId = group.anchorWorkspaceId] in
guard let tabManager,
tabManager.createWorkspaceAdjacent(to: anchorId, position: .below) != nil else {
NSSound.beep()
return
}
},
onRunResolvedItem: { [weak tabManager, groupId = group.id] item in
guard let tabManager else { return }
SidebarWorkspaceGroupContextMenuRunner.run(
Expand Down
Loading
Loading