Remove Fork Agent Session from the Chat UI menu (#38646)

* Remove whole-session fork action from chat menus

* test: drop a terminal menu test this change can't fail
This commit is contained in:
Brennan Benson
2026-11-05 14:38:10 -08:00
committed by GitHub
parent cc91e5d17e
commit 1e5e6df6dc
5 changed files with 40 additions and 72 deletions
@@ -45,12 +45,14 @@ function Chat({
acceptsText = true,
rewind,
onFind,
actions
actions,
standalone = false
}: {
acceptsText?: boolean
rewind?: NativeChatRewindSurface
onFind?: () => void
actions?: Partial<NativeChatContextMenuActions>
standalone?: boolean
}): React.JSX.Element {
const rootRef = useRef<HTMLDivElement>(null)
const composerRef = useRef<NativeChatComposerHandle | null>({
@@ -68,6 +70,20 @@ function Chat({
composerRef,
actions: { ...emptyNativeChatContextMenuActions, ...actions, onPaste: vi.fn() },
showTerminalPaneActions: actions !== undefined,
chatPane: standalone
? { tabId: 'chat-tab', worktreeId: 'wt-1', groupId: 'group-1', sessionId: 'session-1' }
: undefined,
chatSession: standalone
? {
target: { kind: 'local' },
sessionId: 'session-1',
worktreeId: 'wt-1',
epoch: 'epoch-1',
agent: 'claude',
messages: [],
focusSource: vi.fn()
}
: undefined,
fileLinkContext: linkContext,
rewind,
onFind
@@ -108,6 +124,23 @@ afterEach(() => {
vi.resetAllMocks()
})
it.each(['terminal-backed', 'standalone'] as const)(
'omits the whole-session fork action from the %s chat menu',
async (surface) => {
render(
<Chat
actions={surface === 'terminal-backed' ? {} : undefined}
standalone={surface === 'standalone'}
/>
)
await rightClick('Empty area')
expect(screen.getByRole('menuitem', { name: 'Copy Context' })).toBeInTheDocument()
expect(screen.queryByRole('menuitem', { name: 'Fork Agent Session…' })).not.toBeInTheDocument()
}
)
it.each([
{
firstText: 'docs link',
@@ -9,7 +9,7 @@ import {
} from 'react'
import { flushSync } from 'react-dom'
import { chatMathSelectionText } from '../sidebar/chat-markdown-math-selection'
import { Clipboard, Copy, GitFork, Image as ImageIcon, SquareTerminal } from 'lucide-react'
import { Clipboard, Copy, Image as ImageIcon, SquareTerminal } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
@@ -101,7 +101,6 @@ export type NativeChatContextMenuActions = {
onToggleExpand: () => void
canContinueAgentSessionInNewSession: boolean
onContinueAgentSessionInNewSession: () => void
onForkAgentSession: () => void
onCopyAgentSessionContext: () => void
onSetTitle: () => void
canClearPaneTitle: boolean
@@ -124,7 +123,6 @@ export const emptyNativeChatContextMenuActions: Omit<NativeChatContextMenuAction
onToggleExpand: () => {},
canContinueAgentSessionInNewSession: false,
onContinueAgentSessionInNewSession: () => {},
onForkAgentSession: () => {},
onCopyAgentSessionContext: () => {},
onSetTitle: () => {},
canClearPaneTitle: false,
@@ -337,13 +335,6 @@ export function useNativeChatContextMenu({
onSelect={actions.onContinueAgentSessionInNewSession}
/>
) : null}
<DropdownMenuItem onSelect={actions.onForkAgentSession}>
<GitFork />
{translate(
'auto.components.terminal.pane.TerminalContextMenu.8a7ddb8b8a',
'Fork Agent Session…'
)}
</DropdownMenuItem>
<AgentSessionContextCopyMenuItem onSelect={actions.onCopyAgentSessionContext} />
</>
) : null}
@@ -1,13 +1,8 @@
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import { executionHostIdForStructuredTarget } from '@/runtime/structured-agent-session-owner'
import { translate } from '@/i18n/i18n'
import { buildAgentSessionForkPrompt } from '@/lib/agent-session-fork-context'
import { isTuiAgent } from '../../../../shared/agent-launch/catalog/tui-agent-config'
import type { AgentType } from '../../../../shared/agent-status/contracts/agent-status-types'
import type { PreparedAgentSessionFork } from '../terminal-pane/terminal-agent-session-fork'
import { copyAgentSessionContext } from '@/lib/copy-agent-session-context'
import { TerminalAgentSessionForkDialog } from '../terminal-pane/TerminalAgentSessionForkDialog'
import {
resolveNativeChatSessionContext,
type NativeChatSessionContextSource
@@ -22,7 +17,6 @@ export type NativeChatSessionMenuSource = Omit<NativeChatSessionContextSource, '
export function useNativeChatSessionContextActions(
source: NativeChatSessionMenuSource | undefined
) {
const [fork, setFork] = useState<PreparedAgentSessionFork | null>(null)
const [pending, setPending] = useState(false)
const operation = useRef(0)
const busy = useRef(false)
@@ -34,11 +28,10 @@ export function useNativeChatSessionContextActions(
retire()
busy.current = false
setPending(false)
setFork(null)
return retire
}, [source?.sessionId, hostId, source?.epoch, source?.worktreeId, retire])
const prepare = async (action: 'fork' | 'copy'): Promise<void> => {
const copy = async (): Promise<void> => {
if (!source?.epoch || busy.current) {
return
}
@@ -54,26 +47,7 @@ export function useNativeChatSessionContextActions(
'components.native-chat.contextMenu.noSessionContext',
'No chat context is available.'
)
if (action === 'copy') {
await copyAgentSessionContext(context.capturedText, source.focusSource, noContext)
return
}
const prompt = buildAgentSessionForkPrompt({
capturedText: context.capturedText,
sourceLabel: source.sessionId,
agentLabel: source.agent
})
if (!prompt) {
toast.error(noContext)
source.focusSource()
return
}
setFork({
prompt,
agent: isTuiAgent(source.agent) ? source.agent : null,
worktreeId: source.worktreeId,
focusSource: source.focusSource
})
await copyAgentSessionContext(context.capturedText, source.focusSource, noContext)
} finally {
if (operation.current === current) {
busy.current = false
@@ -84,14 +58,6 @@ export function useNativeChatSessionContextActions(
return {
available: Boolean(source?.epoch),
pending,
onFork: () => void prepare('fork'),
onCopy: () => void prepare('copy'),
dialog: fork ? (
<TerminalAgentSessionForkDialog
open={fork !== null}
fork={fork}
onOpenChange={(open) => !open && setFork(null)}
/>
) : null
onCopy: () => void copy()
}
}
@@ -1,6 +1,3 @@
import { GitFork } from 'lucide-react'
import { DropdownMenuItem } from '@/components/ui/dropdown-menu'
import { translate } from '@/i18n/i18n'
import { NativeChatCliForkMenuItems } from '../tab-bar/NativeChatCliForkMenuItems'
import { AgentSessionContextCopyMenuItem } from '../terminal-pane/AgentSessionContextCopyMenuItem'
import { useNativeChatQuickCommandsMenu } from './use-native-chat-quick-commands-menu'
@@ -26,19 +23,7 @@ export function useNativeChatStandaloneMenu({
pane && source ? (
<>
{context.available ? (
<>
<DropdownMenuItem disabled={context.pending} onSelect={context.onFork}>
<GitFork />
{translate(
'auto.components.terminal.pane.TerminalContextMenu.8a7ddb8b8a',
'Fork Agent Session…'
)}
</DropdownMenuItem>
<AgentSessionContextCopyMenuItem
disabled={context.pending}
onSelect={context.onCopy}
/>
</>
<AgentSessionContextCopyMenuItem disabled={context.pending} onSelect={context.onCopy} />
) : null}
<NativeChatCliForkMenuItems
tab={{ id: pane.tabId, worktreeId: pane.worktreeId }}
@@ -49,11 +34,6 @@ export function useNativeChatStandaloneMenu({
/>
</>
) : null,
dialogs: (
<>
{quickCommands.dialog}
{context.dialog}
</>
)
dialogs: quickCommands.dialog
}
}
@@ -54,8 +54,6 @@ export function TerminalPaneNativeChatPortal({
),
onContinueAgentSessionInNewSession: () =>
contextMenu.runForPane(chatPane.id, contextMenu.onContinueAgentSessionInNewSession),
onForkAgentSession: () =>
void contextMenu.runForPane(chatPane.id, contextMenu.onForkAgentSession),
onCopyAgentSessionContext: () =>
void contextMenu.runForPane(chatPane.id, contextMenu.onCopyAgentSessionContext),
onSetTitle: () => contextMenu.runForPane(chatPane.id, contextMenu.onSetTitle),