Skip to content

Add Fork Conversation action to tab context menu - #135

Merged
azooz2003-bit merged 1 commit into
mainfrom
feat-fork-conversation
May 27, 2026
Merged

azooz2003-bit merged 1 commit into
mainfrom
feat-fork-conversation

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented May 27, 2026 •

Copy link
Copy Markdown

Summary

  • New .forkConversation case in TabContextAction
  • New host-provided tabContextForkConversationAvailabilityProvider on BonsplitController
  • Menu item added to TabContextMenuBuilder (shown only when the provider returns true)
  • English + Japanese strings, test fixture updated for the new canForkConversation field

Used by cmux (see manaflow-ai/cmux companion PR) to surface a Fork Conversation entry when a tab is running an active forkable Claude or Codex session.

Test plan

  • swift build --build-tests clean
  • Visual: right-click a terminal tab whose host returns true for the provider, confirm "Fork Conversation" appears

🤖 Generated with Claude Code


Summary by cubic

Adds a “Fork Conversation” action to the tab context menu, shown only when the host says it’s available. This lets apps surface forking for tabs running an active, forkable agent session.

  • New Features

    • Added TabContextAction.forkConversation.
    • Added BonsplitController.tabContextForkConversationAvailabilityProvider to control visibility.
    • Menu item appears only when the provider returns true.
    • Added English and Japanese strings.
  • Migration

    • If you switch exhaustively on TabContextAction, add a .forkConversation case (or a default).
    • Implement the new provider to enable the item for tabs with forkable sessions.

Written for commit 082bf66. Summary will update on new commits. Review in cubic


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Added "Fork Conversation" action to the tab context menu, allowing users to fork conversations directly from the interface without additional navigation. The feature is fully localized in English and Japanese. Host applications can selectively enable this action on a per-conversation basis.

Review Change Stack

Exposes a host-provided availability provider and a new `.forkConversation`
case so embedders (cmux) can surface a "Fork Conversation" entry in the tab
right-click menu when the tab hosts an agent session worth forking. Item is
hidden when the provider returns false, mirroring the existing browser-only
items pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented May 27, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2caf915d-0dac-4878-962a-99347015441f

📥 Commits

Reviewing files that changed from the base of the PR and between cda75f9 and 082bf66.

📒 Files selected for processing (7)
  • Sources/Bonsplit/Internal/Views/TabBarView.swift
  • Sources/Bonsplit/Internal/Views/TabItemView.swift
  • Sources/Bonsplit/Public/BonsplitController.swift
  • Sources/Bonsplit/Public/Types/TabContextAction.swift
  • Sources/Bonsplit/Resources/en.lproj/Localizable.strings
  • Sources/Bonsplit/Resources/ja.lproj/Localizable.strings
  • Tests/BonsplitTests/BonsplitTests.swift

📝 Walkthrough

Walkthrough

This PR adds a "Fork Conversation" action to tab context menus. It introduces a host-provided availability provider on BonsplitController, extends internal state and menu rendering to support the action conditionally, and includes English and Japanese localizations plus a test fix.

Changes

Fork Conversation Tab Context Menu Action

Layer / File(s) Summary
Public API and action type
Sources/Bonsplit/Public/Types/TabContextAction.swift, Sources/Bonsplit/Public/BonsplitController.swift
New forkConversation enum case added to TabContextAction, and new @ObservationIgnored callback property tabContextForkConversationAvailabilityProvider on BonsplitController lets the host determine whether the action is available for a given tab and pane.
Internal state and context menu state wiring
Sources/Bonsplit/Internal/Views/TabBarView.swift
TabContextMenuState gains a canForkConversation boolean field. TabBarView.contextMenuState(for:at:) wires it by calling the provider callback, defaulting to false when absent.
Menu UI construction
Sources/Bonsplit/Internal/Views/TabItemView.swift
TabContextMenuBuilder.makeMenu conditionally inserts a separator and "Fork Conversation" menu action when state.canForkConversation is true, dispatching the .forkConversation tab context action.
Localization and test update
Sources/Bonsplit/Resources/en.lproj/Localizable.strings, Sources/Bonsplit/Resources/ja.lproj/Localizable.strings, Tests/BonsplitTests/BonsplitTests.swift
English localization tabContext.forkConversation → "Fork Conversation" and Japanese equivalent 会話をフォーク are added. Existing test updated to pass the new canForkConversation: false state field.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 A fork in the conversation path,
With strings both bright and Japanese,
The menu grows with availability,
And state flows true where it should be. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a fork conversation action to the tab context menu, which is the primary feature across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-fork-conversation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 27, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a "Fork Conversation" entry to the tab context menu, surfaced only when a new host-provided tabContextForkConversationAvailabilityProvider closure returns true for a given tab/pane pair. The action is dispatched through the existing didRequestTabContextAction delegate path, so no new delegate method is required.

  • TabContextAction.forkConversation case added; routing via performContextAction and the existing String raw-value lookup works without changes.
  • TabContextMenuState.canForkConversation is computed at menu-build time by calling the provider (consistent with the tabContextMoveDestinationsProvider pattern); separator placement in the menu builder avoids double-separator in all conditional combinations.
  • English and Japanese localisation strings added; test fixture updated, though no test covers the canForkConversation: true code path.

Confidence Score: 4/5

Safe to merge; all changes follow established patterns and the build is clean.

The implementation is consistent with the existing provider/action patterns and introduces no new delegate surface. The only gap is that the canForkConversation: true branch — menu item appearance, enabled state, and .forkConversation action dispatch — has no automated test coverage, so a regression in that path could go undetected.

Tests/BonsplitTests/BonsplitTests.swift — the new canForkConversation: true menu path is not exercised by any test.

Important Files Changed

Filename Overview
Sources/Bonsplit/Public/Types/TabContextAction.swift Adds forkConversation case to the TabContextAction enum — minimal, consistent with existing cases, and the String raw value routing in performContextAction handles it automatically.
Sources/Bonsplit/Public/BonsplitController.swift Adds tabContextForkConversationAvailabilityProvider closure property following the exact same pattern as the existing tabContextMoveDestinationsProvider; no concerns.
Sources/Bonsplit/Internal/Views/TabBarView.swift Adds canForkConversation field to TabContextMenuState and computes it by calling the provider at menu-build time; consistent with existing state fields.
Sources/Bonsplit/Internal/Views/TabItemView.swift Inserts the Fork Conversation menu item (with separator) only when canForkConversation is true; separator placement is consistent and no double-separator edge case arises, but the new path lacks a dedicated unit test.
Tests/BonsplitTests/BonsplitTests.swift Fixture updated for the new canForkConversation field, but no test covers the canForkConversation: true path — menu item appearance and action dispatch go untested.
Sources/Bonsplit/Resources/en.lproj/Localizable.strings Adds tabContext.forkConversation English localisation string; correct.
Sources/Bonsplit/Resources/ja.lproj/Localizable.strings Adds tabContext.forkConversation Japanese localisation string; correct.

Sequence Diagram

sequenceDiagram
    participant User
    participant TabBarView
    participant Provider as tabContextForkConversationAvailabilityProvider
    participant TabContextMenuBuilder
    participant Delegate as BonsplitDelegate

    User->>TabBarView: right-click tab
    TabBarView->>Provider: (TabID, PaneID) → Bool
    Provider-->>TabBarView: true / false
    TabBarView->>TabBarView: "build TabContextMenuState (canForkConversation = result)"
    TabBarView->>TabContextMenuBuilder: makeMenu(snapshot, target)
    alt "canForkConversation == true"
        TabContextMenuBuilder->>TabContextMenuBuilder: addSeparator + add Fork Conversation item
    end
    TabContextMenuBuilder-->>User: NSMenu displayed
    User->>TabContextMenuBuilder: click Fork Conversation
    TabContextMenuBuilder->>Delegate: didRequestTabContextAction(.forkConversation, tab, pane)
Loading

Reviews (1): Last reviewed commit: "Add Fork Conversation action to tab cont..." | Re-trigger Greptile

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.

@azooz2003-bit
azooz2003-bit merged commit e24bb9c into main May 27, 2026
6 checks passed
azooz2003-bit added a commit to manaflow-ai/cmux that referenced this pull request May 28, 2026
* Add Fork Conversation to tab right-click menu

Adds a `Fork Conversation` item to the bonsplit tab context menu. It appears
only when the tab is running an agent session that supports forking without
a probe (Claude, Codex). On click, opens a new sibling tab to the right of
the source with `claude --resume <id> --fork-session` (or the analogous
Codex form) as the startup input, so divergence is owned by the agent's
native fork support instead of any cmux-side history copy.

Companion to manaflow-ai/bonsplit#135 which adds the menu item plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Lock down Fork Conversation isolation + handler guard

Addresses PR #4888 review feedback:

- Annotate `liveAgentIndexCache` / `liveAgentIndexCacheLoadedAt` / their
  accessor with `@MainActor` so the implicit MainActor isolation from the
  enclosing `Workspace` class is explicit at the static-storage level. The
  provider closure already runs during SwiftUI body evaluation on the main
  actor, but the explicit annotation prevents a Swift 6 strict-concurrency
  violation when that mode is enabled (CodeRabbit critical / Greptile P1).

- Tighten the bonsplit `.forkConversation` handler to mirror the menu
  visibility gate exactly (`== .supportedWithoutProbe`) instead of the
  weaker `!= .unsupported`. Closes the gap where the action could let a
  `.requiresProbe` snapshot through if ever wired up outside the controlled
  menu (Greptile summary note).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit tests for Fork Conversation tab action

Covers the new Workspace API and the bonsplit context-menu wiring without
needing to drive the NSMenu UI (which XCUI can't reach reliably):

- forkAgentConversationToNewTab creates a sibling tab in the same pane,
  not a split pane, with the snapshot's `--fork-session` command as its
  initial input.
- The forked tab lands immediately to the right of its source even when
  there are other tabs further right.
- canForkAgentConversationFromPanel flips on once a restored Claude
  snapshot is associated with the panel.
- The bonsplit `.forkConversation` action dispatches end-to-end through
  splitTabBar(_:didRequestTabContextAction:for:inPane:) to the same
  forkAgentConversationToNewTab path, locking in the menu wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix Fork Conversation tests: use public TabID API

CI test target failed to compile because the tests reached into
`TabID.id` (which is `internal` in bonsplit), accessible only inside the
bonsplit module via `@testable`. Local `./scripts/reload.sh --tag` builds
the main app but not the test target, which is why the issue slipped
through locally.

Compare TabIDs directly (TabID is Hashable) instead of unwrapping the
underlying UUID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Background-refresh live agent index; add Codex parity test

Addresses two PR #4888 review findings:

- Greptile P1: `RestorableAgentSessionIndex.load()` performs blocking
  `sysctl(KERN_PROCARGS2)` per hook record (for live-PID filtering),
  which is N×2 syscalls on `@MainActor` per right-click within the TTL
  window. Replace the synchronous main-actor load with an off-main
  `Task.detached(priority: .utility)` refresh, keyed on a 1s freshness
  window. The provider reads the cached value synchronously and tolerates
  a slightly-stale snapshot — claude's `--resume`/`--fork-session` still
  works against the persisted transcript even if the cmux-recorded PID
  has died, so showing the menu item briefly for a recently-terminated
  session is benign. First right-click after a fresh Claude launch may
  miss the item until the background refresh completes (~hundreds of
  ms); subsequent SwiftUI body re-renders pick up the new snapshot.

- CodeRabbit: add a Codex-parity test that drives the same context-menu
  dispatcher path with a Codex snapshot and asserts the sibling tab is
  created with the Codex `--fork-session` command. (The Claude-path test
  already asserted focus on the new tab.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Trigger immediate UI refresh after live-index load

Addresses PR #4888 CodeRabbit follow-up: the previous static cache
returned nil on the first call after a stale-out and relied on a later
SwiftUI body refresh (hover/focus) to pick up the new index, which is
why the first right-click could miss the menu item.

Move the cache off the static slot and onto the Workspace instance as
a `@Published` property. When the background `Task.detached` completes
and writes the new index, `objectWillChange` fires on the workspace,
ContentView re-renders, and bonsplit's TabBarView re-evaluates its
context-menu state on the same frame — so "Fork Conversation" becomes
visible the moment the index lands, without a second right-click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Beep when Fork Conversation tab creation fails silently

Greptile P1 (PR #4888): the bonsplit `.forkConversation` handler
discarded `forkAgentConversationToNewTab`'s return value with `_ = …`,
so if `newTerminalSurface` ever returned nil (e.g. layout reject,
workspace resource constraint) the click produced no visible result and
no audio cue, even though every other early-exit in this action branch
already calls `NSSound.beep()`. Match the existing convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use shared singleton for live agent index across workspaces

PR #4888 CodeRabbit follow-up: the previous per-Workspace `@Published`
cache meant each open workspace performed its own background load of
`RestorableAgentSessionIndex.load()`, redundantly running the same JSON
+ per-PID work N times for N open workspaces. Replace with a single
`SharedLiveAgentIndex.shared` actor-isolated singleton that loads once
and serves every workspace; each Workspace subscribes to its
`objectWillChange` in its initializer and forwards it as its own so the
SwiftUI re-render path stays intact (ContentView re-renders → bonsplit's
TabBarView re-evaluates Fork Conversation visibility on the same frame).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ShubhamPatilsd pushed a commit to emergent-inc/mosaic that referenced this pull request Jul 9, 2026
* Add Fork Conversation to tab right-click menu

Adds a `Fork Conversation` item to the bonsplit tab context menu. It appears
only when the tab is running an agent session that supports forking without
a probe (Claude, Codex). On click, opens a new sibling tab to the right of
the source with `claude --resume <id> --fork-session` (or the analogous
Codex form) as the startup input, so divergence is owned by the agent's
native fork support instead of any cmux-side history copy.

Companion to manaflow-ai/bonsplit#135 which adds the menu item plumbing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Lock down Fork Conversation isolation + handler guard

Addresses PR #4888 review feedback:

- Annotate `liveAgentIndexCache` / `liveAgentIndexCacheLoadedAt` / their
  accessor with `@MainActor` so the implicit MainActor isolation from the
  enclosing `Workspace` class is explicit at the static-storage level. The
  provider closure already runs during SwiftUI body evaluation on the main
  actor, but the explicit annotation prevents a Swift 6 strict-concurrency
  violation when that mode is enabled (CodeRabbit critical / Greptile P1).

- Tighten the bonsplit `.forkConversation` handler to mirror the menu
  visibility gate exactly (`== .supportedWithoutProbe`) instead of the
  weaker `!= .unsupported`. Closes the gap where the action could let a
  `.requiresProbe` snapshot through if ever wired up outside the controlled
  menu (Greptile summary note).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Add unit tests for Fork Conversation tab action

Covers the new Workspace API and the bonsplit context-menu wiring without
needing to drive the NSMenu UI (which XCUI can't reach reliably):

- forkAgentConversationToNewTab creates a sibling tab in the same pane,
  not a split pane, with the snapshot's `--fork-session` command as its
  initial input.
- The forked tab lands immediately to the right of its source even when
  there are other tabs further right.
- canForkAgentConversationFromPanel flips on once a restored Claude
  snapshot is associated with the panel.
- The bonsplit `.forkConversation` action dispatches end-to-end through
  splitTabBar(_:didRequestTabContextAction:for:inPane:) to the same
  forkAgentConversationToNewTab path, locking in the menu wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix Fork Conversation tests: use public TabID API

CI test target failed to compile because the tests reached into
`TabID.id` (which is `internal` in bonsplit), accessible only inside the
bonsplit module via `@testable`. Local `./scripts/reload.sh --tag` builds
the main app but not the test target, which is why the issue slipped
through locally.

Compare TabIDs directly (TabID is Hashable) instead of unwrapping the
underlying UUID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Background-refresh live agent index; add Codex parity test

Addresses two PR #4888 review findings:

- Greptile P1: `RestorableAgentSessionIndex.load()` performs blocking
  `sysctl(KERN_PROCARGS2)` per hook record (for live-PID filtering),
  which is N×2 syscalls on `@MainActor` per right-click within the TTL
  window. Replace the synchronous main-actor load with an off-main
  `Task.detached(priority: .utility)` refresh, keyed on a 1s freshness
  window. The provider reads the cached value synchronously and tolerates
  a slightly-stale snapshot — claude's `--resume`/`--fork-session` still
  works against the persisted transcript even if the cmux-recorded PID
  has died, so showing the menu item briefly for a recently-terminated
  session is benign. First right-click after a fresh Claude launch may
  miss the item until the background refresh completes (~hundreds of
  ms); subsequent SwiftUI body re-renders pick up the new snapshot.

- CodeRabbit: add a Codex-parity test that drives the same context-menu
  dispatcher path with a Codex snapshot and asserts the sibling tab is
  created with the Codex `--fork-session` command. (The Claude-path test
  already asserted focus on the new tab.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Trigger immediate UI refresh after live-index load

Addresses PR #4888 CodeRabbit follow-up: the previous static cache
returned nil on the first call after a stale-out and relied on a later
SwiftUI body refresh (hover/focus) to pick up the new index, which is
why the first right-click could miss the menu item.

Move the cache off the static slot and onto the Workspace instance as
a `@Published` property. When the background `Task.detached` completes
and writes the new index, `objectWillChange` fires on the workspace,
ContentView re-renders, and bonsplit's TabBarView re-evaluates its
context-menu state on the same frame — so "Fork Conversation" becomes
visible the moment the index lands, without a second right-click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Beep when Fork Conversation tab creation fails silently

Greptile P1 (PR #4888): the bonsplit `.forkConversation` handler
discarded `forkAgentConversationToNewTab`'s return value with `_ = …`,
so if `newTerminalSurface` ever returned nil (e.g. layout reject,
workspace resource constraint) the click produced no visible result and
no audio cue, even though every other early-exit in this action branch
already calls `NSSound.beep()`. Match the existing convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Use shared singleton for live agent index across workspaces

PR #4888 CodeRabbit follow-up: the previous per-Workspace `@Published`
cache meant each open workspace performed its own background load of
`RestorableAgentSessionIndex.load()`, redundantly running the same JSON
+ per-PID work N times for N open workspaces. Replace with a single
`SharedLiveAgentIndex.shared` actor-isolated singleton that loads once
and serves every workspace; each Workspace subscribes to its
`objectWillChange` in its initializer and forwards it as its own so the
SwiftUI re-render path stays intact (ContentView re-renders → bonsplit's
TabBarView re-evaluates Fork Conversation visibility on the same frame).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant