Skip to content
Merged
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
2 changes: 2 additions & 0 deletions Sources/Bonsplit/Internal/Views/TabBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions Sources/Bonsplit/Internal/Views/TabItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions Sources/Bonsplit/Public/BonsplitController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)?
Expand Down
1 change: 1 addition & 0 deletions Sources/Bonsplit/Public/Types/TabContextAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ public enum TabContextAction: String, CaseIterable, Sendable {
case markAsRead
case markAsUnread
case toggleZoom
case forkConversation
}

public struct TabContextMoveDestination: Identifiable, Equatable, Sendable {
Expand Down
1 change: 1 addition & 0 deletions Sources/Bonsplit/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
"tabContext.pinTab" = "Pin Tab";
"tabContext.markTabAsRead" = "Mark Tab as Read";
"tabContext.markTabAsUnread" = "Mark Tab as Unread";
"tabContext.forkConversation" = "Fork Conversation";
1 change: 1 addition & 0 deletions Sources/Bonsplit/Resources/ja.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@
"tabContext.pinTab" = "タブをピン留め";
"tabContext.markTabAsRead" = "タブを既読にする";
"tabContext.markTabAsUnread" = "タブを未読にする";
"tabContext.forkConversation" = "会話をフォーク";
1 change: 1 addition & 0 deletions Tests/BonsplitTests/BonsplitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1558,6 +1558,7 @@ final class BonsplitTests: XCTestCase {
canMoveToNewWorkspace: true,
canMoveToLeftPane: false,
canMoveToRightPane: true,
canForkConversation: false,

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 Missing test coverage for the true path

The existing test only exercises canForkConversation: false. No test verifies that when the field is true the "Fork Conversation" item actually appears in the built menu, is enabled, and — when activated via target.performContextAction — dispatches exactly .forkConversation to the onContextAction handler. Without this, a future accidental guard inversion (e.g. if !state.canForkConversation) or a typo in the representedObject raw value would go undetected by the suite.

isZoomed: false,
hasSplits: true,
shortcuts: [:]
Expand Down
Loading