-
-
Notifications
You must be signed in to change notification settings - Fork 944
Add Chrome-style tab groups #2641
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
64b8954
5188b46
7d7a858
c1e82e4
d5c9110
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,9 @@ DerivedData/ | |
| *.xcuserstate | ||
| xcuserdata/ | ||
|
|
||
| # Development plans | ||
| PLAN.md | ||
|
|
||
| # macOS | ||
| .DS_Store | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -336,6 +336,16 @@ extension Workspace { | |||||||
| applyProcessTitle(snapshot.processTitle) | ||||||||
| setCustomTitle(snapshot.customTitle) | ||||||||
| setCustomDescription(snapshot.customDescription) | ||||||||
|
|
||||||||
| // Before syncing workspace color, adopt any workspace group restored from | ||||||||
| // the snapshot so syncWorkspaceGroup updates it instead of creating a duplicate. | ||||||||
| if let colorHex = snapshot.customColor?.replacingOccurrences(of: "#", with: ""), | ||||||||
| let firstPane = bonsplitController.allPaneIds.first { | ||||||||
| let restored = bonsplitController.groups(inPane: firstPane) | ||||||||
| if let match = restored.first(where: { $0.colorHex == colorHex }) { | ||||||||
| workspaceGroupId = match.id | ||||||||
| } | ||||||||
| } | ||||||||
| setCustomColor(snapshot.customColor) | ||||||||
| isPinned = snapshot.isPinned | ||||||||
|
|
||||||||
|
|
@@ -374,10 +384,31 @@ extension Workspace { | |||||||
| case .pane(let pane): | ||||||||
| let panelIds = sessionPanelIDs(for: pane) | ||||||||
| let selectedPanelId = pane.selectedTabId.flatMap(sessionPanelID(forExternalTabIDString:)) | ||||||||
|
|
||||||||
| // Capture all tab groups for this pane (including workspace-managed groups) | ||||||||
| let paneId = PaneID(id: UUID(uuidString: pane.id) ?? UUID()) | ||||||||
| let paneGroups = bonsplitController.groups(inPane: paneId) | ||||||||
| let groupSnapshots: [SessionTabGroupSnapshot]? = paneGroups.isEmpty ? nil : paneGroups.map { | ||||||||
| SessionTabGroupSnapshot(id: $0.id, name: $0.name, colorHex: $0.colorHex, isCollapsed: $0.isCollapsed ? true : nil) | ||||||||
| } | ||||||||
|
|
||||||||
| var panelGroupAssignments: [UUID: UUID]? = nil | ||||||||
| let tabs = bonsplitController.tabs(inPane: paneId) | ||||||||
| let assignments = tabs.compactMap { tab -> (UUID, UUID)? in | ||||||||
| guard let gid = tab.groupId, | ||||||||
| let panelId = panelIdFromSurfaceId(tab.id) else { return nil } | ||||||||
| return (panelId, gid) | ||||||||
| } | ||||||||
| if !assignments.isEmpty { | ||||||||
| panelGroupAssignments = Dictionary(assignments, uniquingKeysWith: { _, last in last }) | ||||||||
| } | ||||||||
|
|
||||||||
| return .pane( | ||||||||
| SessionPaneLayoutSnapshot( | ||||||||
| panelIds: panelIds, | ||||||||
| selectedPanelId: selectedPanelId | ||||||||
| selectedPanelId: selectedPanelId, | ||||||||
| groups: groupSnapshots, | ||||||||
| panelGroupAssignments: panelGroupAssignments | ||||||||
| ) | ||||||||
| ) | ||||||||
| case .split(let split): | ||||||||
|
|
@@ -634,6 +665,27 @@ extension Workspace { | |||||||
| bonsplitController.focusPane(paneId) | ||||||||
| bonsplitController.selectTab(selectedTabId) | ||||||||
| } | ||||||||
|
|
||||||||
| // Restore tab groups | ||||||||
| if let savedGroups = snapshot.groups { | ||||||||
| for groupSnapshot in savedGroups { | ||||||||
| bonsplitController.createGroup( | ||||||||
| id: groupSnapshot.id, | ||||||||
| name: groupSnapshot.name, | ||||||||
| colorHex: groupSnapshot.colorHex, | ||||||||
| isCollapsed: groupSnapshot.isCollapsed ?? false, | ||||||||
| inPane: paneId | ||||||||
| ) | ||||||||
| } | ||||||||
|
|
||||||||
| if let assignments = snapshot.panelGroupAssignments { | ||||||||
| for (oldPanelId, groupId) in assignments { | ||||||||
| guard let newPanelId = oldToNewPanelIds[oldPanelId], | ||||||||
| let tabId = surfaceIdFromPanelId(newPanelId) else { continue } | ||||||||
| bonsplitController.assignTab(tabId, toGroup: groupId) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| private func createPanel(from snapshot: SessionPanelSnapshot, inPane paneId: PaneID) -> UUID? { | ||||||||
|
|
@@ -6486,6 +6538,8 @@ final class Workspace: Identifiable, ObservableObject { | |||||||
| @Published var customDescription: String? | ||||||||
| @Published var isPinned: Bool = false | ||||||||
| @Published var customColor: String? // hex string, e.g. "#C0392B" | ||||||||
| /// Group ID for the auto-created workspace group (ties sidebar color to tab groups). | ||||||||
| var workspaceGroupId: UUID? | ||||||||
| @Published var currentDirectory: String | ||||||||
| private(set) var preferredBrowserProfileID: UUID? | ||||||||
|
|
||||||||
|
|
@@ -7520,6 +7574,51 @@ final class Workspace: Identifiable, ObservableObject { | |||||||
| } else { | ||||||||
| customColor = nil | ||||||||
| } | ||||||||
| syncWorkspaceGroup() | ||||||||
| } | ||||||||
|
|
||||||||
| /// Ensures the workspace group exists when the workspace has a color, and | ||||||||
| /// auto-assigns all tabs in every pane to the workspace group. | ||||||||
| func syncWorkspaceGroup() { | ||||||||
| guard let colorHex = customColor?.replacingOccurrences(of: "#", with: "") else { | ||||||||
| // No color → remove workspace group if it exists | ||||||||
| if let gid = workspaceGroupId { | ||||||||
| for paneId in bonsplitController.allPaneIds { | ||||||||
| bonsplitController.deleteGroup(gid, inPane: paneId) | ||||||||
| } | ||||||||
| workspaceGroupId = nil | ||||||||
| } | ||||||||
| return | ||||||||
| } | ||||||||
|
|
||||||||
| let groupName = customTitle ?? title | ||||||||
| if let gid = workspaceGroupId { | ||||||||
| // Update existing group color and name in all panes | ||||||||
| for paneId in bonsplitController.allPaneIds { | ||||||||
| // Ensure the group exists in this pane (may be new from a split) | ||||||||
| if bonsplitController.groups(inPane: paneId).first(where: { $0.id == gid }) == nil { | ||||||||
| bonsplitController.createGroup(id: gid, name: groupName, colorHex: colorHex, inPane: paneId) | ||||||||
| for tab in bonsplitController.tabs(inPane: paneId) where tab.groupId == nil { | ||||||||
| bonsplitController.assignTab(tab.id, toGroup: gid) | ||||||||
| } | ||||||||
| } else { | ||||||||
| bonsplitController.renameGroup(gid, to: groupName, inPane: paneId) | ||||||||
| bonsplitController.changeGroupColor(gid, to: colorHex, inPane: paneId) | ||||||||
| } | ||||||||
| } | ||||||||
| } else { | ||||||||
| // Create workspace group with a single stable ID across all panes. | ||||||||
| // Only assign tabs that aren't already in a group (preserves saved assignments | ||||||||
| // during restore and user-created groups during normal operation). | ||||||||
| let stableId = UUID() | ||||||||
| workspaceGroupId = stableId | ||||||||
| for paneId in bonsplitController.allPaneIds { | ||||||||
| bonsplitController.createGroup(id: stableId, name: groupName, colorHex: colorHex, inPane: paneId) | ||||||||
| for tab in bonsplitController.tabs(inPane: paneId) where tab.groupId == nil { | ||||||||
| bonsplitController.assignTab(tab.id, toGroup: stableId) | ||||||||
| } | ||||||||
| } | ||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| } | ||||||||
| } | ||||||||
|
Comment on lines
+7589
to
7622
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Fix: use a per-pane map (
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
|
||||||||
| private static func normalizedCustomDescription(_ description: String?) -> String? { | ||||||||
|
|
@@ -7540,6 +7639,13 @@ final class Workspace: Identifiable, ObservableObject { | |||||||
| customTitle = trimmed | ||||||||
| self.title = trimmed | ||||||||
| } | ||||||||
| // Keep workspace group name in sync with workspace title | ||||||||
| if let gid = workspaceGroupId { | ||||||||
| let groupName = customTitle ?? self.title | ||||||||
| for paneId in bonsplitController.allPaneIds { | ||||||||
| bonsplitController.renameGroup(gid, to: groupName, inPane: paneId) | ||||||||
| } | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| func setCustomDescription(_ description: String?) { | ||||||||
|
|
@@ -12131,6 +12237,19 @@ extension Workspace: BonsplitDelegate { | |||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| func splitTabBar(_ controller: BonsplitController, didCreateTab tab: Bonsplit.Tab, inPane pane: PaneID) { | ||||||||
| // Auto-assign new tabs to the workspace group when the workspace has a color. | ||||||||
| // Ensure the group exists in this pane first (may be new from a split). | ||||||||
| if let gid = workspaceGroupId, tab.groupId == nil { | ||||||||
| if bonsplitController.groups(inPane: pane).first(where: { $0.id == gid }) == nil { | ||||||||
| let groupName = customTitle ?? title | ||||||||
| let colorHex = customColor?.replacingOccurrences(of: "#", with: "") ?? "" | ||||||||
| bonsplitController.createGroup(id: gid, name: groupName, colorHex: colorHex, inPane: pane) | ||||||||
| } | ||||||||
| bonsplitController.assignTab(tab.id, toGroup: gid) | ||||||||
| } | ||||||||
| } | ||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
|
|
||||||||
| func splitTabBar(_ controller: BonsplitController, didRequestNewTab kind: String, inPane pane: PaneID) { | ||||||||
| switch kind { | ||||||||
| case "terminal": | ||||||||
|
|
@@ -12180,6 +12299,20 @@ extension Workspace: BonsplitDelegate { | |||||||
| case .toggleZoom: | ||||||||
| guard let panelId = panelIdFromSurfaceId(tab.id) else { return } | ||||||||
| toggleSplitZoom(panelId: panelId) | ||||||||
| case .newGroup: | ||||||||
| let colors = ["4F46E5", "DC2626", "059669", "D97706", "7C3AED", "DB2777"] | ||||||||
| let existingCount = bonsplitController.groups(inPane: pane).count | ||||||||
| let color = colors[existingCount % colors.count] | ||||||||
| let groupNumber = existingCount + 1 | ||||||||
| let name = String(localized: "tab.group.defaultName", defaultValue: "Group") + " \(groupNumber)" | ||||||||
| if let groupId = bonsplitController.createGroup(name: name, colorHex: color, inPane: pane) { | ||||||||
| bonsplitController.assignTab(tab.id, toGroup: groupId) | ||||||||
| } | ||||||||
|
Comment on lines
+12302
to
+12310
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Localize the default group name.
🤖 Prompt for AI Agents |
||||||||
| case .addToGroup: | ||||||||
| // Handled via direct bonsplitController.assignTab() from the submenu | ||||||||
|
Comment on lines
+12311
to
+12312
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The default group name
Suggested change
|
||||||||
| break | ||||||||
| case .removeFromGroup: | ||||||||
| bonsplitController.removeTabFromGroup(tab.id) | ||||||||
| @unknown default: | ||||||||
|
Comment on lines
+12314
to
12316
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The |
||||||||
| break | ||||||||
| } | ||||||||
|
|
||||||||
Uh oh!
There was an error while loading. Please reload this page.