diff --git a/Sources/Bonsplit/Internal/Views/TabBarView.swift b/Sources/Bonsplit/Internal/Views/TabBarView.swift index 7ac1234f..0a44e8ac 100644 --- a/Sources/Bonsplit/Internal/Views/TabBarView.swift +++ b/Sources/Bonsplit/Internal/Views/TabBarView.swift @@ -884,6 +884,7 @@ struct TabContextMenuState { let canMoveToNewWorkspace: Bool let canMoveToLeftPane: Bool let canMoveToRightPane: Bool + let canForkConversation: Bool let isZoomed: Bool let hasSplits: Bool let shortcuts: [TabContextAction: KeyboardShortcut] @@ -1386,6 +1387,7 @@ struct TabBarView: View { canMoveToNewWorkspace: controller.allTabIds.count > 1, canMoveToLeftPane: controller.adjacentPane(to: pane.id, direction: .left) != nil, canMoveToRightPane: controller.adjacentPane(to: pane.id, direction: .right) != nil, + canForkConversation: controller.tabContextForkConversationAvailabilityProvider?(TabID(id: tab.id), pane.id) ?? false, isZoomed: splitViewController.zoomedPaneId == pane.id, hasSplits: splitViewController.rootNode.allPaneIds.count > 1, shortcuts: controller.contextMenuShortcuts diff --git a/Sources/Bonsplit/Internal/Views/TabItemView.swift b/Sources/Bonsplit/Internal/Views/TabItemView.swift index 9c067d37..7f167191 100644 --- a/Sources/Bonsplit/Internal/Views/TabItemView.swift +++ b/Sources/Bonsplit/Internal/Views/TabItemView.swift @@ -832,6 +832,17 @@ enum TabContextMenuBuilder { ) } + if state.canForkConversation { + menu.addItem(.separator()) + addAction( + title: localized("tabContext.forkConversation", defaultValue: "Fork Conversation"), + action: .forkConversation, + state: state, + target: target, + to: menu + ) + } + menu.addItem(.separator()) addAction( diff --git a/Sources/Bonsplit/Public/BonsplitController.swift b/Sources/Bonsplit/Public/BonsplitController.swift index 1688e1cc..a5a407c9 100644 --- a/Sources/Bonsplit/Public/BonsplitController.swift +++ b/Sources/Bonsplit/Public/BonsplitController.swift @@ -77,6 +77,11 @@ public final class BonsplitController { /// Host-provided destinations for the tab context menu's Move Tab submenu. @ObservationIgnored public var tabContextMoveDestinationsProvider: ((TabID, PaneID) -> [TabContextMoveDestination])? + /// Host-provided synchronous check that decides whether the tab context menu should + /// surface a "Fork Conversation" action for the tab (e.g. an active forkable agent + /// session). Return `true` to enable the item, `false` (or omit the provider) to hide it. + @ObservationIgnored public var tabContextForkConversationAvailabilityProvider: ((TabID, PaneID) -> Bool)? + /// Called when the user explicitly requests to close a tab from the tab strip UI. /// Internal host-driven closes should not use this hook. @ObservationIgnored public var onTabCloseRequest: ((_ tabId: TabID, _ paneId: PaneID, _ source: TabCloseRequestSource) -> Void)? diff --git a/Sources/Bonsplit/Public/Types/TabContextAction.swift b/Sources/Bonsplit/Public/Types/TabContextAction.swift index 6181f3d5..460679e8 100644 --- a/Sources/Bonsplit/Public/Types/TabContextAction.swift +++ b/Sources/Bonsplit/Public/Types/TabContextAction.swift @@ -20,6 +20,7 @@ public enum TabContextAction: String, CaseIterable, Sendable { case markAsRead case markAsUnread case toggleZoom + case forkConversation } public struct TabContextMoveDestination: Identifiable, Equatable, Sendable { diff --git a/Sources/Bonsplit/Resources/en.lproj/Localizable.strings b/Sources/Bonsplit/Resources/en.lproj/Localizable.strings index 1acd4adb..c5a65a44 100644 --- a/Sources/Bonsplit/Resources/en.lproj/Localizable.strings +++ b/Sources/Bonsplit/Resources/en.lproj/Localizable.strings @@ -18,3 +18,4 @@ "tabContext.pinTab" = "Pin Tab"; "tabContext.markTabAsRead" = "Mark Tab as Read"; "tabContext.markTabAsUnread" = "Mark Tab as Unread"; +"tabContext.forkConversation" = "Fork Conversation"; diff --git a/Sources/Bonsplit/Resources/ja.lproj/Localizable.strings b/Sources/Bonsplit/Resources/ja.lproj/Localizable.strings index 8799ee73..cd520184 100644 --- a/Sources/Bonsplit/Resources/ja.lproj/Localizable.strings +++ b/Sources/Bonsplit/Resources/ja.lproj/Localizable.strings @@ -18,3 +18,4 @@ "tabContext.pinTab" = "タブをピン留め"; "tabContext.markTabAsRead" = "タブを既読にする"; "tabContext.markTabAsUnread" = "タブを未読にする"; +"tabContext.forkConversation" = "会話をフォーク"; diff --git a/Tests/BonsplitTests/BonsplitTests.swift b/Tests/BonsplitTests/BonsplitTests.swift index b9e08fb3..decf8414 100644 --- a/Tests/BonsplitTests/BonsplitTests.swift +++ b/Tests/BonsplitTests/BonsplitTests.swift @@ -1558,6 +1558,7 @@ final class BonsplitTests: XCTestCase { canMoveToNewWorkspace: true, canMoveToLeftPane: false, canMoveToRightPane: true, + canForkConversation: false, isZoomed: false, hasSplits: true, shortcuts: [:]