diff --git a/mobile/app/h/[hostId]/session/[worktreeId].tsx b/mobile/app/h/[hostId]/session/[worktreeId].tsx index 6dcfe9f4af1..7cf1367a993 100644 --- a/mobile/app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/app/h/[hostId]/session/[worktreeId].tsx @@ -229,7 +229,6 @@ import { TerminalPaneView } from '../../../../src/session/TerminalPaneView' import { MobileNativeChatOverlay } from '../../../../src/session/MobileNativeChatOverlay' import { MobileStructuredAgentSessionView } from '../../../../src/session/MobileStructuredAgentSessionView' import { MobileStructuredSessionCreateError } from '../../../../src/session/MobileStructuredSessionCreateError' -import * as mobileStructuredTuiSend from '../../../../src/session/mobile-structured-tui-send' import { useMobileStructuredSessionEntry } from '../../../../src/session/use-mobile-structured-session-entry' import { showMobileStructuredChatChoice } from '../../../../src/session/mobile-structured-session-create' import { MobileBrowserTabActionSheet } from '../../../../src/session/MobileBrowserTabActionSheet' @@ -4676,19 +4675,6 @@ export default function SessionScreen() { } return accepted }} - onTuiSend={(text, restored) => - mobileStructuredTuiSend.sendMobileStructuredTuiComposerMessage({ - client, - connected: connState === 'connected', - agent: activeStructuredTab.agent, - handoff: structuredSessionEntry.session.handoff, - deviceToken: deviceTokenRef.current, - text, - attachments: [...restored, ...structuredSessionEntry.attachments.attachments], - onAccepted: structuredSessionEntry.attachments.clear, - onToast: showToast - }) - } onTakeQueuedForEdit={structuredSessionEntry.writes.takeQueuedForEdit} onRetry={structuredSessionEntry.writes.retry} onRespondToPrompt={structuredSessionEntry.writes.respondToPrompt} @@ -4698,8 +4684,6 @@ export default function SessionScreen() { onAttachImage={() => void structuredSessionEntry.attachments.attach('library')} onRemoveAttachment={structuredSessionEntry.attachments.remove} onCancel={structuredSessionEntry.writes.cancel} - handoff={structuredSessionEntry.session.handoff} - onRequestHandoff={structuredSessionEntry.writes.requestHandoff} /> ) : activePendingTerminalTab ? ( diff --git a/mobile/src/session/MobileStructuredAgentSessionView.test.tsx b/mobile/src/session/MobileStructuredAgentSessionView.test.tsx index 937e6af4bb7..c03a2e3ed96 100644 --- a/mobile/src/session/MobileStructuredAgentSessionView.test.tsx +++ b/mobile/src/session/MobileStructuredAgentSessionView.test.tsx @@ -50,7 +50,6 @@ function options(): MobileNativeChatSessionOptionsController { describe('MobileStructuredAgentSessionView command seam', () => { let renderer: ReactTestRenderer | null = null const onSend = vi.fn(async () => true) - const onTuiSend = vi.fn(async () => true) let controller: MobileNativeChatSessionOptionsController function props( @@ -64,7 +63,6 @@ describe('MobileStructuredAgentSessionView command seam', () => { onLoadOlder: async () => false, outbox: [], onSend, - onTuiSend, onTakeQueuedForEdit: async () => null, onRetry: async () => {}, onRespondToPrompt: async () => true, @@ -74,8 +72,6 @@ describe('MobileStructuredAgentSessionView command seam', () => { onAttachImage: () => {}, onRemoveAttachment: () => {}, onCancel: async () => true, - handoff: null, - onRequestHandoff: async () => true, ...overrides } } @@ -89,7 +85,6 @@ describe('MobileStructuredAgentSessionView command seam', () => { beforeEach(() => { globalThis.IS_REACT_ACT_ENVIRONMENT = true onSend.mockClear() - onTuiSend.mockClear() controller = options() render() }) @@ -114,27 +109,6 @@ describe('MobileStructuredAgentSessionView command seam', () => { expect(onSend).not.toHaveBeenCalled() }) - it('disables zero-turn TUI resume with truthful guidance', () => { - render({ - handoff: { - owner: 'native', - direction: null, - phase: 'idle', - stage: null, - operationId: null - } - }) - - const label = renderer!.root - .findAllByType('Text') - .find((node) => node.children.includes('Open agent TUI')) - expect(label?.parent?.props).toMatchObject({ - disabled: true, - accessibilityState: { disabled: true }, - accessibilityHint: 'Send a message first to open the agent TUI.' - }) - }) - it('renders an explicit refusal for unsupported advertised commands', async () => { await submit('/review') @@ -144,87 +118,9 @@ describe('MobileStructuredAgentSessionView command seam', () => { ).toEqual(['/review is not available in chat sessions.']) }) - it('keeps the composer enabled and routes stable TUI ownership through its bridge', async () => { - act(() => { - renderer!.update( - createElement( - MobileStructuredAgentSessionView, - props({ - handoff: { - owner: 'tui', - direction: null, - phase: 'idle', - stage: null, - operationId: null, - terminal: { handle: 'term-tui', tabId: 'tab-tui', paneKey: 'pane-tui' } - } - }) - ) - ) - }) - - expect(renderer!.root.findByType('MobileNativeChatComposer').props.disabled).toBe(false) - await submit('hello from mobile') - expect(onTuiSend).toHaveBeenCalledWith('hello from mobile', []) - expect(onSend).not.toHaveBeenCalled() - }) - - it('routes stable TUI slash commands to typed terminal dispatch', async () => { - act(() => { - renderer!.update( - createElement( - MobileStructuredAgentSessionView, - props({ - handoff: { - owner: 'tui', - direction: null, - phase: 'idle', - stage: null, - operationId: null, - terminal: { handle: 'term-tui', tabId: 'tab-tui', paneKey: 'pane-tui' } - } - }) - ) - ) - }) - - await submit('/model') - expect(onTuiSend).toHaveBeenCalledWith('/model', []) - expect(controller.invokeAction).not.toHaveBeenCalled() - expect(onSend).not.toHaveBeenCalled() - }) - - it('disables and refuses composer sends while ownership is switching', async () => { - act(() => { - renderer!.update( - createElement( - MobileStructuredAgentSessionView, - props({ - handoff: { - owner: 'native', - direction: 'to-tui', - phase: 'switching', - stage: 'preparing', - operationId: 'handoff-1' - } - }) - ) - ) - }) - - const composer = renderer!.root.findByType('MobileNativeChatComposer') - expect(composer.props.disabled).toBe(true) - await act(async () => { - expect(await composer.props.onSend('racing send')).toBe(false) - }) - expect(onSend).not.toHaveBeenCalled() - expect(onTuiSend).not.toHaveBeenCalled() - }) - it('keeps native ownership on the structured agent-session send path', async () => { await submit('native message') expect(onSend).toHaveBeenCalledWith('native message', []) - expect(onTuiSend).not.toHaveBeenCalled() }) }) diff --git a/mobile/src/session/MobileStructuredAgentSessionView.tsx b/mobile/src/session/MobileStructuredAgentSessionView.tsx index deb4ee2eadf..620e47bebc0 100644 --- a/mobile/src/session/MobileStructuredAgentSessionView.tsx +++ b/mobile/src/session/MobileStructuredAgentSessionView.tsx @@ -11,11 +11,6 @@ import { } from 'react-native' import { Square } from 'lucide-react-native' import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' -import type { - AgentSessionHandoffDirection, - AgentSessionHandoffMode, - AgentSessionHandoffStatus -} from '../../../src/shared/agent-session-wire' import { colors } from '../theme/mobile-theme' import { MobileNativeChatComposer } from './MobileNativeChatComposer' import { MobileNativeChatMessage } from './MobileNativeChatMessage' @@ -33,7 +28,6 @@ import { restoreMobileStructuredAttachments, type MobileStructuredTimelineRow } from './mobile-structured-session-timeline' -import { hasPersistedStructuredAgentSessionTurn } from '../../../src/shared/structured-agent-session-projection' import { admitStructuredOlderPage, beginStructuredUserScroll, @@ -60,7 +54,6 @@ type Props = { onOpenFile?: (path: string) => void outbox: MobileStructuredOutboxEntry[] onSend: (text: string, restored: readonly PendingNativeChatImage[]) => Promise - onTuiSend?: (text: string, restored: readonly PendingNativeChatImage[]) => Promise onTakeQueuedForEdit: (clientMessageId: string) => Promise onRetry: (clientMessageId: string) => Promise onRespondToPrompt: (item: MobileStructuredPromptItem, optionId: string) => Promise @@ -70,12 +63,6 @@ type Props = { onAttachImage: () => void onRemoveAttachment: (id: string) => void onCancel: (turnId: string) => Promise - handoff: AgentSessionHandoffStatus | null - onRequestHandoff: ( - direction: AgentSessionHandoffDirection, - mode: AgentSessionHandoffMode, - action?: 'start' | 'cancel-queued' | 'retry' | 'recover' - ) => Promise } export function MobileStructuredAgentSessionView(props: Props): React.JSX.Element { @@ -88,16 +75,7 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen const [commandError, setCommandError] = useState(null) const turnId = activeMobileStructuredTurnId(props.items) const allAttachments = [...restored, ...props.attachments] - const stableTuiOwner = - props.handoff?.owner === 'tui' && - props.handoff.phase === 'idle' && - Boolean(props.handoff.terminal?.handle) && - Boolean(props.onTuiSend) - const stableNativeOwner = - props.handoff === null || - props.handoff === undefined || - (props.handoff.owner === 'native' && props.handoff.phase === 'idle') - const composerEnabled = props.status === 'ready' && (stableNativeOwner || stableTuiOwner) + const composerEnabled = props.status === 'ready' const rows = useMemo( () => buildMobileStructuredTimeline(props.items, props.outbox), [props.items, props.outbox] @@ -126,12 +104,6 @@ export function MobileStructuredAgentSessionView(props: Props): React.JSX.Elemen return ( - ) } - -function MobileStructuredHandoffBanner(props: { - status: AgentSessionHandoffStatus | null - isWorking: boolean - hasPersistedTurn: boolean - onRequest: Props['onRequestHandoff'] -}): React.JSX.Element | null { - const status = props.status - if (!status) { - return null - } - if (status.owner === 'native' && status.phase === 'idle') { - return ( - - {props.isWorking ? ( - <> - [styles.handoffButton, pressed && styles.pressed]} - onPress={() => void props.onRequest('to-tui', 'after-turn')} - > - Switch after this turn - - [styles.handoffButton, pressed && styles.pressed]} - onPress={() => void props.onRequest('to-tui', 'stop-turn')} - > - Stop turn and switch - - - ) : ( - [ - styles.handoffButton, - !props.hasPersistedTurn && styles.disabled, - pressed && props.hasPersistedTurn && styles.pressed - ]} - disabled={!props.hasPersistedTurn} - accessibilityRole="button" - accessibilityState={{ disabled: !props.hasPersistedTurn }} - accessibilityHint={ - props.hasPersistedTurn ? undefined : 'Send a message first to open the agent TUI.' - } - // A submitted turn can reach the host before isWorking updates; after-turn is immediate when idle. - onPress={() => void props.onRequest('to-tui', 'after-turn')} - > - Open agent TUI - - )} - - ) - } - const copy = - status.phase === 'waiting-for-exit' - ? 'Exit the agent terminal to continue in chat.' - : status.phase === 'switching' - ? mobileHandoffStageCopy(status) - : (status.error?.message ?? - `Agent is open in terminal${status.hostLabel ? ` on ${status.hostLabel}` : ''}`) - return ( - - {copy} - {status.phase === 'queued' && status.direction ? ( - void props.onRequest(status.direction!, 'after-turn', 'cancel-queued')} - > - Cancel - - ) : status.owner === 'tui' && status.phase === 'idle' ? ( - void props.onRequest('to-native', 'after-turn')}> - Return to chat - - ) : status.phase === 'failed' && status.direction && status.error?.canRetryProof ? ( - void props.onRequest(status.direction!, 'now', 'recover')}> - Retry proof - - ) : status.phase === 'failed' && - status.direction && - status.error?.recoverableOwner !== 'none' ? ( - void props.onRequest(status.direction!, 'now', 'retry')}> - Retry - - ) : null} - - ) -} - -function mobileHandoffStageCopy(status: AgentSessionHandoffStatus): string { - switch (status.stage) { - case 'preparing': - return status.direction === 'to-tui' ? 'Finishing chat session…' : 'Finishing agent terminal…' - case 'old-owner-stopped': - return status.direction === 'to-tui' ? 'Opening agent terminal…' : 'Resuming chat session…' - case 'new-owner-proving': - return status.direction === 'to-tui' ? 'Verifying agent terminal…' : 'Verifying chat session…' - case 'recovering': - return 'Recovering agent session…' - case 'manual-recovery': - return 'Agent session needs recovery' - default: - return 'Switching session owner…' - } -} diff --git a/mobile/src/session/mobile-structured-agent-session-view-styles.ts b/mobile/src/session/mobile-structured-agent-session-view-styles.ts index 131e1040be0..4e4f0022e9f 100644 --- a/mobile/src/session/mobile-structured-agent-session-view-styles.ts +++ b/mobile/src/session/mobile-structured-agent-session-view-styles.ts @@ -36,36 +36,10 @@ export const styles = StyleSheet.create({ }, cancelText: { color: colors.statusRed, fontSize: typography.metaSize, fontWeight: '600' }, pressed: { opacity: 0.7 }, - disabled: { opacity: 0.45 }, writeError: { alignItems: 'center', paddingHorizontal: spacing.lg, paddingVertical: spacing.sm }, - writeErrorText: { color: colors.statusRed, fontSize: typography.metaSize }, - handoffBar: { - flexDirection: 'row', - justifyContent: 'flex-end', - alignItems: 'center', - gap: spacing.xs, - paddingHorizontal: spacing.lg, - paddingVertical: spacing.xs - }, - handoffBanner: { - minHeight: 42, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - gap: spacing.sm, - paddingHorizontal: spacing.lg, - paddingVertical: spacing.sm, - backgroundColor: colors.bgRaised, - borderBottomWidth: StyleSheet.hairlineWidth, - borderBottomColor: colors.borderSubtle - }, - handoffError: { backgroundColor: colors.bgRaised }, - handoffText: { flex: 1, color: colors.textSecondary, fontSize: typography.metaSize }, - handoffErrorText: { flex: 1, color: colors.statusRed, fontSize: typography.metaSize }, - handoffButton: { paddingHorizontal: spacing.sm, paddingVertical: spacing.xs }, - handoffButtonText: { color: colors.textPrimary, fontSize: typography.metaSize, fontWeight: '600' } + writeErrorText: { color: colors.statusRed, fontSize: typography.metaSize } }) diff --git a/mobile/src/session/mobile-structured-tui-send.test.ts b/mobile/src/session/mobile-structured-tui-send.test.ts deleted file mode 100644 index 5c7dc5fe262..00000000000 --- a/mobile/src/session/mobile-structured-tui-send.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest' -import type { RpcClient } from '../transport/rpc-client' -import { resetMobileNativeChatStaleInputForTests } from './mobile-native-chat-stale-input' -import { resetMobileNativeChatTerminalWritesForTests } from './mobile-native-chat-terminal-write-lock' -import { - sendMobileStructuredTuiComposerMessage, - sendMobileStructuredTuiMessage -} from './mobile-structured-tui-send' - -function acceptingClient(): RpcClient { - return { - sendRequest: vi.fn().mockResolvedValue({ - id: 'send', - ok: true, - result: { send: { accepted: true } }, - _meta: { runtimeId: 'runtime' } - }) - } as unknown as RpcClient -} - -afterEach(() => { - vi.useRealTimers() - resetMobileNativeChatStaleInputForTests() - resetMobileNativeChatTerminalWritesForTests() -}) - -describe('sendMobileStructuredTuiMessage', () => { - it('routes stable TUI composer text through terminal.send', async () => { - const client = acceptingClient() - - await expect( - sendMobileStructuredTuiMessage({ - client, - terminal: 'term-tui', - agent: 'codex', - deviceToken: 'mobile-1', - text: 'hello', - attachments: [] - }) - ).resolves.toBe('accepted') - - expect(client.sendRequest).toHaveBeenCalledWith( - 'terminal.send', - { - terminal: 'term-tui', - text: '\x15hello', - enter: true, - client: { id: 'mobile-1', type: 'mobile' } - }, - expect.objectContaining({ budgetSpansConnect: true }) - ) - }) - - it('types stable TUI slash commands one key at a time', async () => { - vi.useFakeTimers() - const client = acceptingClient() - - const sending = sendMobileStructuredTuiMessage({ - client, - terminal: 'term-tui', - agent: 'codex', - deviceToken: null, - text: '/model', - attachments: [] - }) - await vi.runAllTimersAsync() - await expect(sending).resolves.toBe('accepted') - - expect( - vi - .mocked(client.sendRequest) - .mock.calls.map(([, params]) => (params as { text: string }).text) - ).toEqual(['\x15', '/', 'm', 'o', 'd', 'e', 'l', '\r']) - expect( - vi - .mocked(client.sendRequest) - .mock.calls.every( - ([method, params]) => - method === 'terminal.send' && (params as { enter: boolean }).enter === false - ) - ).toBe(true) - }) - - it('types Claude-only slash commands through the provider command path', async () => { - vi.useFakeTimers() - const client = acceptingClient() - - const sending = sendMobileStructuredTuiMessage({ - client, - terminal: 'term-claude', - agent: 'claude', - deviceToken: null, - text: '/clear', - attachments: [] - }) - await vi.runAllTimersAsync() - await expect(sending).resolves.toBe('accepted') - - expect( - vi - .mocked(client.sendRequest) - .mock.calls.map(([, params]) => (params as { text: string }).text) - ).toEqual(['\x15', '/', 'c', 'l', 'e', 'a', 'r', '\r']) - }) - - it('settles the composer only after its proven TUI owner accepts the send', async () => { - const onAccepted = vi.fn() - - await expect( - sendMobileStructuredTuiComposerMessage({ - client: acceptingClient(), - connected: true, - agent: 'claude', - handoff: { - owner: 'tui', - direction: null, - phase: 'idle', - stage: null, - operationId: null, - terminal: { handle: 'term-claude', tabId: 'tab-claude', paneKey: 'pane-claude' } - }, - deviceToken: null, - text: 'hello', - attachments: [], - onAccepted, - onToast: vi.fn() - }) - ).resolves.toBe(true) - expect(onAccepted).toHaveBeenCalledOnce() - }) -}) diff --git a/mobile/src/session/mobile-structured-tui-send.ts b/mobile/src/session/mobile-structured-tui-send.ts deleted file mode 100644 index 9adc14093f4..00000000000 --- a/mobile/src/session/mobile-structured-tui-send.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { isSlashCommandDraft } from '../../../src/shared/native-chat-slash-commands' -import type { AgentSessionHandoffStatus } from '../../../src/shared/agent-session-wire' -import type { RpcClient } from '../transport/rpc-client' -import type { PendingNativeChatImage } from './mobile-native-chat-image-attachment' -import { - MOBILE_NATIVE_CHAT_IMAGE_SETTLE_MS, - pasteMobileNativeChatImagePaths -} from './mobile-native-chat-image-send' -import { - openMobileNativeChatSendBudget, - sendMobileNativeChatMessageWithOutcome, - typeMobileNativeChatCommandWithOutcome, - type MobileNativeChatSendOutcome -} from './mobile-native-chat-send' -import { classifyMobileNativeChatSend } from './mobile-native-chat-send-classification' -import { - clearMobileNativeChatInputStale, - healMobileNativeChatStaleInput, - markMobileNativeChatInputStale -} from './mobile-native-chat-stale-input' -import { - acquireMobileNativeChatTerminalWrite, - releaseMobileNativeChatTerminalWrite -} from './mobile-native-chat-terminal-write-lock' - -type MobileStructuredTuiSendArgs = { - client: RpcClient - terminal: string - agent: 'claude' | 'codex' - deviceToken: string | null - text: string - attachments: readonly PendingNativeChatImage[] - sleep?: (ms: number) => Promise -} - -const wait = (ms: number): Promise => - new Promise((resolve) => { - setTimeout(resolve, ms) - }) - -export function getMobileStructuredTuiTerminal( - handoff: AgentSessionHandoffStatus | null -): string | null { - return handoff?.owner === 'tui' && handoff.phase === 'idle' - ? (handoff.terminal?.handle ?? null) - : null -} - -/** Sends the structured transcript composer through its proven TUI owner. */ -export async function sendMobileStructuredTuiMessage( - args: MobileStructuredTuiSendArgs -): Promise { - if (!acquireMobileNativeChatTerminalWrite(args.terminal)) { - return 'rejected' - } - try { - const deadline = openMobileNativeChatSendBudget() - const mobileClient = args.deviceToken - ? { id: args.deviceToken, type: 'mobile' as const } - : undefined - if ( - !(await healMobileNativeChatStaleInput({ - client: args.client, - terminal: args.terminal, - deviceToken: args.deviceToken, - deadline - })) - ) { - return 'rejected' - } - if (args.attachments.length > 0) { - const pasted = await pasteMobileNativeChatImagePaths({ - client: args.client, - terminal: args.terminal, - deviceToken: args.deviceToken, - imagePaths: args.attachments.map((attachment) => attachment.path), - deadline - }) - if (!pasted) { - markMobileNativeChatInputStale(args.terminal) - return 'rejected' - } - clearMobileNativeChatInputStale(args.terminal) - await (args.sleep ?? wait)(MOBILE_NATIVE_CHAT_IMAGE_SETTLE_MS) - } - const textDeadline = - args.attachments.length > 0 ? deadline + MOBILE_NATIVE_CHAT_IMAGE_SETTLE_MS : deadline - const classification = classifyMobileNativeChatSend(args.agent, args.text) - const outcome = - classification !== 'chat' && isSlashCommandDraft(args.text) && args.attachments.length === 0 - ? await typeMobileNativeChatCommandWithOutcome({ - client: args.client, - terminal: args.terminal, - command: args.text, - ...(mobileClient ? { mobileClient } : {}), - deadline: textDeadline - }) - : await sendMobileNativeChatMessageWithOutcome({ - client: args.client, - terminal: args.terminal, - text: args.text, - clearInputFirst: args.attachments.length === 0, - ...(mobileClient ? { mobileClient } : {}), - deadline: textDeadline - }) - if (args.attachments.length > 0 && outcome !== 'accepted') { - markMobileNativeChatInputStale(args.terminal) - } - return outcome - } catch { - if (args.attachments.length > 0) { - markMobileNativeChatInputStale(args.terminal) - } - return 'rejected' - } finally { - releaseMobileNativeChatTerminalWrite(args.terminal) - } -} - -export async function sendMobileStructuredTuiComposerMessage(args: { - client: RpcClient | null - connected: boolean - agent: 'claude' | 'codex' - handoff: AgentSessionHandoffStatus | null - deviceToken: string | null - text: string - attachments: readonly PendingNativeChatImage[] - onAccepted: () => void - onToast: (message: string, durationMs: number) => void -}): Promise { - const terminal = getMobileStructuredTuiTerminal(args.handoff) - if (!args.client || !args.connected || !terminal) { - args.onToast('Message not sent (disconnected)', 1800) - return false - } - const outcome = await sendMobileStructuredTuiMessage({ - client: args.client, - terminal, - agent: args.agent, - deviceToken: args.deviceToken, - text: args.text, - attachments: args.attachments - }) - if (outcome === 'rejected') { - args.onToast('Message not sent', 1800) - return false - } - args.onAccepted() - if (outcome === 'unknown') { - args.onToast('Delivery unconfirmed — check chat before retrying', 2400) - } - return true -} diff --git a/mobile/src/session/use-mobile-structured-agent-session.ts b/mobile/src/session/use-mobile-structured-agent-session.ts index aaa46cd4854..cf8fe08522c 100644 --- a/mobile/src/session/use-mobile-structured-agent-session.ts +++ b/mobile/src/session/use-mobile-structured-agent-session.ts @@ -2,7 +2,6 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'r import { AppState, type AppStateStatus } from 'react-native' import type { AgentSessionHistoryResult, - AgentSessionHandoffStatus, AgentSessionSubscribeEvent } from '../../../src/shared/agent-session-wire' import type { AgentJournalCursor } from '../../../src/shared/agent-session-journal-types' @@ -58,7 +57,6 @@ export function useMobileStructuredAgentSession(args: { hasOlder: boolean loadingOlder: boolean loadOlder: () => Promise - handoff: AgentSessionHandoffStatus | null } { const { client, sessionId } = args const [state, dispatch] = useReducer(reduceStructuredAgentSession, EMPTY_STRUCTURED_AGENT_SESSION) @@ -136,23 +134,15 @@ export function useMobileStructuredAgentSession(args: { } }) } - void Promise.all([ - client.sendRequest('agentSession.history', { sessionId, direction: 'tail', limit: 40 }), - client.sendRequest('agentSession.handoffStatus', { sessionId }).catch(() => null) - ]) - .then(([response, handoffResponse]) => { + void client + .sendRequest('agentSession.history', { sessionId, direction: 'tail', limit: 40 }) + .then((response) => { if (closed) { return } if (!response.ok) { throw new Error(response.error.message) } - if (handoffResponse?.ok) { - dispatch({ - type: 'handoff', - handoff: handoffResponse.result as AgentSessionHandoffStatus - }) - } const result = response.result as AgentSessionHistoryResult if (result.ok) { dispatch({ type: 'tail-page', page: result.page }) @@ -250,7 +240,6 @@ export function useMobileStructuredAgentSession(args: { error: state.error || undefined, hasOlder: state.hasOlder, loadingOlder, - loadOlder, - handoff: state.handoff + loadOlder } } diff --git a/mobile/src/session/use-mobile-structured-session-entry.ts b/mobile/src/session/use-mobile-structured-session-entry.ts index 78ad18ffe2d..5e641e7468a 100644 --- a/mobile/src/session/use-mobile-structured-session-entry.ts +++ b/mobile/src/session/use-mobile-structured-session-entry.ts @@ -49,8 +49,7 @@ export function useMobileStructuredSessionEntry(args: { connected, sessionId, fence: session.fence, - submissions: session.submissions, - handoffOperationId: session.handoff?.operationId + submissions: session.submissions }) const sessionOptions = useMobileStructuredSessionOptions({ client, diff --git a/mobile/src/session/use-mobile-structured-session-mutations.ts b/mobile/src/session/use-mobile-structured-session-mutations.ts index 6a7c4c45347..0a0c4a470c0 100644 --- a/mobile/src/session/use-mobile-structured-session-mutations.ts +++ b/mobile/src/session/use-mobile-structured-session-mutations.ts @@ -6,8 +6,6 @@ import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types' import type { - AgentSessionHandoffDirection, - AgentSessionHandoffMode, AgentSessionMutationResult, AgentSessionOptionResult, AgentSessionPromptResult @@ -29,18 +27,12 @@ export type MobileStructuredSessionMutations = { respondToPrompt: (item: MobileStructuredPromptItem, optionId: string) => Promise setOption: (key: string, value: string) => Promise cancel: (turnId: string) => Promise - requestHandoff: ( - direction: AgentSessionHandoffDirection, - mode: AgentSessionHandoffMode, - action?: 'start' | 'cancel-queued' | 'retry' | 'recover' - ) => Promise } export function useMobileStructuredSessionMutations(args: { client: RpcClient | null sessionId: string | null fence: number | null - handoffOperationId?: string | null onRefusal: (message: string | null) => void }): MobileStructuredSessionMutations { const operationIdsRef = useRef(new Map()) @@ -142,22 +134,9 @@ export function useMobileStructuredSessionMutations(args: { value }), cancel: async (turnId: string) => - Boolean(await mutate('agentSession.cancel', 'agentSession.cancel', { turnId })), - requestHandoff: async ( - direction: AgentSessionHandoffDirection, - mode: AgentSessionHandoffMode, - action: 'start' | 'cancel-queued' | 'retry' | 'recover' = 'start' - ) => - Boolean( - await mutate( - 'agentSession.requestHandoff', - 'agentSession.requestHandoff', - { direction, mode, action }, - action === 'retry' ? args.handoffOperationId : null - ) - ) + Boolean(await mutate('agentSession.cancel', 'agentSession.cancel', { turnId })) }), - [args.handoffOperationId, mutate] + [mutate] ) } diff --git a/mobile/src/session/use-mobile-structured-session-writes.test.ts b/mobile/src/session/use-mobile-structured-session-writes.test.ts index 8d4cd4f862a..be74d1144c3 100644 --- a/mobile/src/session/use-mobile-structured-session-writes.test.ts +++ b/mobile/src/session/use-mobile-structured-session-writes.test.ts @@ -652,56 +652,6 @@ describe('useMobileStructuredSessionWrites', () => { expect(crypto.randomUUID).toHaveBeenCalledTimes(1) }) - it('mints a fresh handoff operation after a settled refusal', async () => { - sendRequest - .mockResolvedValueOnce({ - id: 'handoff-stale', - ok: true, - _meta: { runtimeId: 'runtime-1' }, - result: { - ok: false, - refusal: { - code: 'agent_session_checkpoint_stale', - message: 'runtime fence advanced', - currentFence: 4 - } - } - }) - .mockResolvedValueOnce({ - id: 'handoff-started', - ok: true, - _meta: { runtimeId: 'runtime-1' }, - result: { - ok: true, - replayed: false, - fence: 3, - cursor: { epoch: 'epoch-1', sequence: 2 }, - value: { - status: { - owner: 'native', - direction: 'to-tui', - phase: 'switching', - stage: 'preparing', - operationId: 'second-operation' - } - } - } - }) - - await act(async () => { - expect(await api!.requestHandoff('to-tui', 'now')).toBe(false) - expect(await api!.requestHandoff('to-tui', 'now')).toBe(true) - }) - - const operationIds = sendRequest.mock.calls.map( - ([, params]) => - (params as { envelope: { clientOperationId: string } }).envelope.clientOperationId - ) - expect(operationIds[0]).toMatch(/-00000000000040008000000000000001$/) - expect(operationIds[1]).toMatch(/-00000000000040008000000000000002$/) - expect(operationIds[1]).not.toBe(operationIds[0]) - }) - it('isolates mutation ids and late errors across session changes', async () => { const first = deferred() sendRequest.mockImplementationOnce(() => first.promise).mockResolvedValueOnce(accepted('b')) diff --git a/mobile/src/session/use-mobile-structured-session-writes.ts b/mobile/src/session/use-mobile-structured-session-writes.ts index 344460e56a4..ea0ed059dfd 100644 --- a/mobile/src/session/use-mobile-structured-session-writes.ts +++ b/mobile/src/session/use-mobile-structured-session-writes.ts @@ -31,7 +31,6 @@ export function useMobileStructuredSessionWrites(args: { sessionId: string | null fence: number | null submissions: readonly AgentJournalSubmission[] - handoffOperationId?: string | null }): MobileStructuredSessionWrites { const { client, connected, sessionId, fence, submissions } = args const [outbox, setOutbox] = useState([]) @@ -50,7 +49,6 @@ export function useMobileStructuredSessionWrites(args: { client, sessionId, fence, - handoffOperationId: args.handoffOperationId, onRefusal: setError }) const outboxMutations = useMobileStructuredOutboxMutations({ diff --git a/src/main/runtime/rpc/methods/structured-agent-session.test.ts b/src/main/runtime/rpc/methods/structured-agent-session.test.ts index a60f0ca5da5..69bfc057d7a 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.test.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.test.ts @@ -191,7 +191,7 @@ describe('capability gating', () => { for (const method of STRUCTURED_AGENT_SESSION_METHODS) { expect(names).toContain(method.name) } - expect(STRUCTURED_AGENT_SESSION_METHODS).toHaveLength(14) + expect(STRUCTURED_AGENT_SESSION_METHODS).toHaveLength(13) }) it('hides the surface from a declared client that did not advertise it', async () => { @@ -316,35 +316,16 @@ describe('method routing', () => { ]) }) - it('routes a mobile reverse request through the host-owned handoff coordinator', async () => { - const params = { - envelope: envelope(), - direction: 'to-native', - mode: 'after-turn', - action: 'start' - } - const response = await call('agentSession.requestHandoff', params, { - ...STRUCTURED_CLIENT, - clientId: 'mobile-device-a' - }) - - expect(response).toMatchObject({ ok: true }) - expect(hostCalls.requestHandoff).toHaveBeenCalledWith({ callerKey: 'mobile-device-a' }, params) - }) - - it('accepts manual proof recovery at the RPC boundary', async () => { - const params = { + it('does not register the structured handoff mutation', async () => { + const response = await call('agentSession.requestHandoff', { envelope: envelope(), direction: 'to-tui', mode: 'now', - action: 'recover' - } + action: 'start' + }) - await expect(call('agentSession.requestHandoff', params)).resolves.toMatchObject({ ok: true }) - expect(hostCalls.requestHandoff).toHaveBeenCalledWith( - { callerKey: 'trusted-local:runtime' }, - params - ) + expect(response).toMatchObject({ ok: false, error: { code: 'method_not_found' } }) + expect(hostCalls.requestHandoff).not.toHaveBeenCalled() }) }) diff --git a/src/main/runtime/rpc/methods/structured-agent-session.ts b/src/main/runtime/rpc/methods/structured-agent-session.ts index 1225eeb7ea2..cde0156c062 100644 --- a/src/main/runtime/rpc/methods/structured-agent-session.ts +++ b/src/main/runtime/rpc/methods/structured-agent-session.ts @@ -21,7 +21,6 @@ import { CreateParams, CreateSupportParams, HistoryParams, - HandoffParams, HandoffStatusParams, OptionsParams, RespondParams, @@ -209,11 +208,6 @@ export const STRUCTURED_AGENT_SESSION_METHODS: RpcAnyMethod[] = [ params: SetOptionParams, handler: async (params, ctx) => requireHost(ctx).setOption(callerFor(ctx), params) }), - defineMethod({ - name: 'agentSession.requestHandoff', - params: HandoffParams, - handler: async (params, ctx) => requireHost(ctx).requestHandoff(callerFor(ctx), params) - }), defineMethod({ name: 'agentSession.handoffStatus', params: HandoffStatusParams, diff --git a/src/main/runtime/runtime-rpc.ts b/src/main/runtime/runtime-rpc.ts index 008a6f54a51..860e1db7e9f 100644 --- a/src/main/runtime/runtime-rpc.ts +++ b/src/main/runtime/runtime-rpc.ts @@ -190,14 +190,12 @@ const MOBILE_RPC_METHOD_ALLOWLIST = new Set([ 'agentSession.create', 'agentSession.createSupport', 'agentSession.cancel', - 'agentSession.handoffStatus', 'agentSession.history', 'agentSession.options', 'agentSession.respondToApproval', 'agentSession.respondToQuestion', 'agentSession.send', 'agentSession.setOption', - 'agentSession.requestHandoff', 'agentSession.subscribe', 'agentSession.unsubscribe', 'browser.back', diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx index 6a26f3d5f3f..540db7afef8 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx @@ -17,7 +17,6 @@ import { useNativeChatFileLinkClick } from './use-native-chat-file-link-click' import { useNativeChatFileLinkContext } from './use-native-chat-file-link-context' import { useStructuredAgentSession } from './use-structured-agent-session' import { translate } from '@/i18n/i18n' -import { StructuredAgentSessionHandoffChrome } from './StructuredAgentSessionHandoffChrome' function encodeQuestionAnswer(questionId: string, answer: string): string { return `${encodeURIComponent(questionId)}:${encodeURIComponent(answer)}` @@ -100,14 +99,6 @@ export function NativeChatStructuredSession(props: { tabIndex={-1} className="flex h-full min-h-0 w-full flex-col bg-background focus:outline-none" > - { - void controller.requestHandoff(direction, mode, action) - }} - />
{viewState.kind === 'loading' ? ( @@ -203,9 +194,7 @@ export function NativeChatStructuredSession(props: { {controller.error ?? composerError}

) : null} - {prompt || - controller.handoff?.owner === 'tui' || - controller.handoff?.phase === 'switching' ? null : ( + {prompt ? null : ( { - it('uses queued-safe admission when the native view still appears idle', () => { - const onRequest = vi.fn() - render( - , - { wrapper: TooltipProvider } - ) - - fireEvent.click(screen.getByRole('button', { name: 'Open agent TUI' })) - - expect(onRequest).toHaveBeenCalledWith('to-tui', 'after-turn') - }) - - it('disables zero-turn TUI resume with truthful guidance', async () => { - render( - , - { wrapper: TooltipProvider } - ) - - const button = screen.getByRole('button', { name: 'Open agent TUI' }) - expect(button.hasAttribute('disabled')).toBe(true) - fireEvent.pointerMove(button.parentElement!) - expect( - await screen.findAllByText('Send a message first to open the agent TUI.') - ).not.toHaveLength(0) - }) - - it('offers one Retry action for a recoverable dead TUI owner', () => { - const onRequest = vi.fn() - render( - , - { wrapper: TooltipProvider } - ) - - expect(screen.queryByRole('button', { name: 'Return to chat' })).toBeNull() - fireEvent.click(screen.getByRole('button', { name: 'Retry' })) - expect(onRequest).toHaveBeenCalledWith('to-native', 'now', 'retry') - }) -}) diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx deleted file mode 100644 index 23c4f1e1d9d..00000000000 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionHandoffChrome.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import type { - AgentSessionHandoffDirection, - AgentSessionHandoffMode, - AgentSessionHandoffStatus -} from '../../../../shared/agent-session-wire' -import { Button } from '@/components/ui/button' -import { Badge } from '@/components/ui/badge' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' -import { translate } from '@/i18n/i18n' - -type Props = { - status: AgentSessionHandoffStatus | null - isWorking: boolean - hasPersistedTurn: boolean - onRequest: ( - direction: AgentSessionHandoffDirection, - mode: AgentSessionHandoffMode, - action?: 'start' | 'cancel-queued' | 'retry' | 'recover' - ) => void -} - -function handoffStageCopy(status: AgentSessionHandoffStatus): string { - if (status.stage === 'preparing') { - return status.direction === 'to-tui' - ? translate('components.native-chat.handoff.stage.finishingChat', 'Finishing chat session…') - : translate( - 'components.native-chat.handoff.stage.finishingTerminal', - 'Finishing agent terminal…' - ) - } - if (status.stage === 'old-owner-stopped') { - return status.direction === 'to-tui' - ? translate('components.native-chat.handoff.stage.openingTerminal', 'Opening agent terminal…') - : translate('components.native-chat.handoff.stage.resumingChat', 'Resuming chat session…') - } - if (status.stage === 'new-owner-proving') { - return status.direction === 'to-tui' - ? translate( - 'components.native-chat.handoff.stage.verifyingTerminal', - 'Verifying agent terminal…' - ) - : translate('components.native-chat.handoff.stage.verifyingChat', 'Verifying chat session…') - } - if (status.stage === 'recovering') { - return translate('components.native-chat.handoff.stage.recovering', 'Recovering agent session…') - } - if (status.stage === 'manual-recovery') { - return translate( - 'components.native-chat.handoff.stage.manualRecovery', - 'Agent session needs recovery' - ) - } - return translate('components.native-chat.handoff.switchingOwner', 'Switching session owner…') -} - -export function StructuredAgentSessionHandoffChrome({ - status, - isWorking, - hasPersistedTurn, - onRequest -}: Props): React.JSX.Element | null { - if (!status) { - return null - } - const owner = status?.owner ?? 'native' - const phase = status?.phase ?? 'idle' - const switching = phase === 'switching' || phase === 'waiting-for-exit' - return ( - <> -
- - {switching - ? translate('components.native-chat.handoff.mode.switching', 'Switching') - : owner === 'tui' - ? translate('components.native-chat.handoff.mode.terminal', 'Terminal') - : translate('components.native-chat.handoff.mode.chat', 'Chat')} - -
- {phase === 'queued' && status?.direction ? ( - <> - - {status.direction === 'to-tui' - ? translate( - 'components.native-chat.handoff.switchingAfterTurn', - 'Switching after this turn' - ) - : translate( - 'components.native-chat.handoff.returningAfterTurn', - 'Returning after this turn' - )} - - - - ) : owner === 'native' && phase === 'idle' ? ( - isWorking ? ( - <> - - - - ) : ( - - - - - - - {!hasPersistedTurn ? ( - - {translate( - 'components.native-chat.handoff.sendFirst', - 'Send a message first to open the agent TUI.' - )} - - ) : null} - - ) - ) : owner === 'tui' && phase === 'idle' ? ( - - ) : null} -
-
- {owner === 'tui' && phase === 'idle' ? ( -
- - {status?.hostLabel - ? translate( - 'components.native-chat.handoff.agentOpenOnHost', - 'Agent is open in terminal on {{value0}}.', - { value0: status.hostLabel } - ) - : translate('components.native-chat.handoff.agentOpen', 'Agent is open in terminal.')} - - -
- ) : null} - {switching ? ( -
- {phase === 'waiting-for-exit' - ? translate( - 'components.native-chat.handoff.exitTerminal', - 'Exit the agent terminal to continue in chat.' - ) - : status?.stage - ? handoffStageCopy(status) - : translate( - 'components.native-chat.handoff.switchingOwner', - 'Switching session owner…' - )} -
- ) : null} - {phase === 'failed' && status?.error ? ( -
-
- {status.error.message} - {status.direction && status.error.canRetryProof ? ( - - ) : status.direction && status.error.recoverableOwner !== 'none' ? ( - - ) : null} -
- {status.error.details ? ( -
- {translate('components.native-chat.handoff.details', 'Details')} -

{status.error.details}

-
- ) : null} -
- ) : null} - - ) -} diff --git a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx index 90e71ca32e0..5cd15a70023 100644 --- a/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx +++ b/src/renderer/src/components/native-chat/StructuredAgentSessionStatusBridge.tsx @@ -1,9 +1,6 @@ import { useEffect, useMemo } from 'react' import { useShallow } from 'zustand/react/shallow' -import type { - AgentSessionHandoffStatus, - AgentSessionHistoryResult -} from '../../../../shared/agent-session-wire' +import type { AgentSessionHistoryResult } from '../../../../shared/agent-session-wire' import { projectStructuredAgentSessionStatus, structuredAgentSessionPaneKey @@ -22,10 +19,6 @@ import { callStructuredAgentSession, subscribeStructuredAgentSession } from '@/runtime/structured-agent-session-client' -import { - clearStructuredHandoff, - publishStructuredHandoff -} from '@/runtime/structured-agent-session-handoff-store' type StructuredTab = Tab & { contentType: 'agent-session' } @@ -94,13 +87,6 @@ function startStatusProjection(tab: StructuredTab, target: RuntimeClientTarget): target, { sessionId: tab.entityId, ...(state.cursor ? { cursor: state.cursor } : {}) }, (event) => { - if ('handoff' in event && event.handoff) { - publishStructuredHandoff({ - sessionId: tab.entityId, - fence: event.fence ?? state.fence ?? 0, - status: event.handoff - }) - } if ( event.type === 'batch' && !shouldAdvanceStructuredResumeCursor(state.cursor, event.batch.cursor) @@ -141,17 +127,12 @@ function startStatusProjection(tab: StructuredTab, target: RuntimeClientTarget): opening = false } } - void Promise.all([ - callStructuredAgentSession(target, 'agentSession.history', { - sessionId: tab.entityId, - direction: 'tail', - limit: 40 - }), - callStructuredAgentSession(target, 'agentSession.handoffStatus', { - sessionId: tab.entityId - }).catch(() => null) - ]) - .then(async ([result, handoff]) => { + void callStructuredAgentSession(target, 'agentSession.history', { + sessionId: tab.entityId, + direction: 'tail', + limit: 40 + }) + .then(async (result) => { if (stopped) { return } @@ -169,14 +150,6 @@ function startStatusProjection(tab: StructuredTab, target: RuntimeClientTarget): } }) } - if (handoff) { - apply({ type: 'handoff', handoff }) - publishStructuredHandoff({ - sessionId: tab.entityId, - fence: result.ok ? (result.page.fence ?? 0) : (result.fence ?? 0), - status: handoff - }) - } await open() }) .catch(scheduleReconnect) @@ -186,7 +159,6 @@ function startStatusProjection(tab: StructuredTab, target: RuntimeClientTarget): clearTimeout(reconnectTimer) } unsubscribe() - clearStructuredHandoff(tab.entityId) useAppStore.getState().removeAgentStatus(structuredAgentSessionPaneKey(tab.id, tab.entityId)) } } diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-read.ts b/src/renderer/src/components/native-chat/use-structured-agent-session-read.ts index f775799b646..59f2fcc5001 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-read.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-read.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useReducer, useRef, useState } from 'react' import type { - AgentSessionHandoffStatus, AgentSessionHistoryResult, AgentSessionSubscribeEvent } from '../../../../shared/agent-session-wire' @@ -130,18 +129,11 @@ export function useStructuredAgentSessionRead(args: { } } async function refreshTail(): Promise { - const [result, handoff] = await Promise.all([ - callStructuredAgentSession(target, 'agentSession.history', { - sessionId, - direction: 'tail', - limit: 40 - }), - callStructuredAgentSession( - target, - 'agentSession.handoffStatus', - { sessionId } - ).catch(() => null) - ]) + const result = await callStructuredAgentSession( + target, + 'agentSession.history', + { sessionId, direction: 'tail', limit: 40 } + ) if (stopped) { return } @@ -161,9 +153,6 @@ export function useStructuredAgentSessionRead(args: { }) resumeCursorRef.current = result.snapshot.cursor } - if (handoff) { - dispatch({ type: 'handoff', handoff }) - } } const refreshOnFocus = (): void => { if (!document.hasFocus()) { diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx b/src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx index 04fa10f32ff..3090fc638c7 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx +++ b/src/renderer/src/components/native-chat/use-structured-agent-session.test.tsx @@ -259,65 +259,6 @@ describe('useStructuredAgentSession options', () => { expect(mocks.operationId).toHaveBeenCalledTimes(1) }) - it('mints a fresh handoff operation after a settled refusal', async () => { - let attempts = 0 - mocks.call.mockImplementation((_target, method) => { - if (method === 'agentSession.options') { - return Promise.resolve(OPTIONS) - } - attempts += 1 - return Promise.resolve( - attempts === 1 - ? { - ok: false, - refusal: { - code: 'agent_session_checkpoint_stale', - message: 'runtime fence advanced', - currentFence: 4 - } - } - : { - ok: true, - replayed: false, - value: { - status: { - owner: 'native', - direction: 'to-tui', - phase: 'switching', - stage: 'preparing', - operationId: 'operation-2' - } - } - } - ) - }) - const { result } = renderHook(() => - useStructuredAgentSession({ - sessionId: 'session-1', - target: LOCAL_TARGET, - agent: 'codex' - }) - ) - await waitFor(() => expect(result.current.optionSnapshot).toHaveLength(2)) - - await act(async () => { - expect(await result.current.requestHandoff('to-tui', 'now')).toBeNull() - expect(await result.current.requestHandoff('to-tui', 'now')).toMatchObject({ - status: { phase: 'switching' } - }) - }) - - const mutations = mocks.call.mock.calls.filter( - ([, method]) => method === 'agentSession.requestHandoff' - ) - expect( - mutations.map( - ([, , params]) => - (params as { envelope: { clientOperationId: string } }).envelope.clientOperationId - ) - ).toEqual(['operation-1', 'operation-2']) - }) - it('ignores an option failure from a superseded fence', async () => { let reject!: (error: Error) => void const pending = new Promise((_resolve, rejectPromise) => { diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session.ts b/src/renderer/src/components/native-chat/use-structured-agent-session.ts index 9cae83bf79e..db411ffc9de 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-session.ts @@ -3,9 +3,6 @@ import type { AgentJournalRenderItem } from '../../../../shared/agent-session-jo import type { AgentType } from '../../../../shared/agent-status-types' import type { AgentSessionMutationResult, - AgentSessionHandoffDirection, - AgentSessionHandoffMode, - AgentSessionHandoffResult, AgentSessionOptionResult, AgentSessionOptionsResult, AgentSessionPromptResult @@ -23,7 +20,6 @@ import { } from '../../../../shared/structured-agent-session-options' import { activeStructuredAgentSessionTurnId, - hasPersistedStructuredAgentSessionTurn, projectStructuredItemsToNativeChat } from '../../../../shared/structured-agent-session-projection' import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client' @@ -234,7 +230,6 @@ export function useStructuredAgentSession(args: { send: outboxController.send, retry: outboxController.retry, isWorking: turnId !== null, - hasPersistedTurn: hasPersistedStructuredAgentSessionTurn(state.items), turnId, cancel: (turnId: string) => mutate('agentSession.cancel', 'agentSession.cancel', { turnId }), respond: (item: StructuredPromptItem, optionId: string) => @@ -247,18 +242,6 @@ export function useStructuredAgentSession(args: { ), optionSnapshot, optionSurface, - setStructuredOption, - requestHandoff: ( - direction: AgentSessionHandoffDirection, - mode: AgentSessionHandoffMode, - action: 'start' | 'cancel-queued' | 'retry' | 'recover' = 'start' - ) => - mutate( - 'agentSession.requestHandoff', - 'agentSession.requestHandoff', - { direction, mode, action }, - action === 'retry' ? stateRef.current.handoff?.operationId : null - ), - handoff: state.handoff + setStructuredOption } } diff --git a/src/renderer/src/components/terminal-pane/StructuredAgentSessionTerminalReturnButton.tsx b/src/renderer/src/components/terminal-pane/StructuredAgentSessionTerminalReturnButton.tsx deleted file mode 100644 index bba08bd914c..00000000000 --- a/src/renderer/src/components/terminal-pane/StructuredAgentSessionTerminalReturnButton.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button } from '@/components/ui/button' -import { translate } from '@/i18n/i18n' - -export function StructuredAgentSessionTerminalReturnButton(props: { - enabled: boolean - onReturn?: () => void -}): React.JSX.Element | null { - if (!props.enabled) { - return null - } - return ( - - ) -} diff --git a/src/renderer/src/components/terminal-pane/TerminalPane.tsx b/src/renderer/src/components/terminal-pane/TerminalPane.tsx index a1f418c9326..8861b076de9 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPane.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPane.tsx @@ -246,13 +246,6 @@ import { } from './regular-terminal-focus-ownership' import { useTerminalQuickCommandHosts } from '@/hooks/use-terminal-quick-command-hosts' import { refreshTerminalImeInputContext } from './terminal-ime-input-context-refresh' -import { useTerminalStructuredHandoff } from '@/runtime/structured-agent-session-handoff-store' -import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client' -import { getActiveRuntimeTarget } from '@/runtime/runtime-rpc-client' -import { - createStructuredAgentSessionOperationId, - structuredAgentSessionPayloadFingerprint -} from '../../../../shared/structured-agent-session-mutation' type TerminalPaneProps = { tabId: string @@ -350,34 +343,6 @@ function TerminalPane( const isRendererVisible = isVisible && isWorktreeActive const isVisibleRef = useRef(isRendererVisible) isVisibleRef.current = isRendererVisible - const structuredHandoff = useTerminalStructuredHandoff(tabId) - const returnStructuredSession = useCallback(() => { - if (!structuredHandoff) { - return - } - const direction = 'to-native' as const - const mode = 'after-turn' as const - const action = 'start' as const - const sessionId = structuredHandoff.sessionId - const environmentId = getRuntimeEnvironmentIdForWorktree(useAppStore.getState(), worktreeId) - const target = getActiveRuntimeTarget({ activeRuntimeEnvironmentId: environmentId }) - const clientOperationId = createStructuredAgentSessionOperationId(() => crypto.randomUUID()) - void callStructuredAgentSession(target, 'agentSession.requestHandoff', { - envelope: { - sessionId, - clientOperationId, - expectedRuntimeFence: structuredHandoff.fence, - payloadFingerprint: structuredAgentSessionPayloadFingerprint({ - method: 'agentSession.requestHandoff', - sessionId, - fields: { direction, mode, action } - }) - }, - direction, - mode, - action - }) - }, [structuredHandoff, worktreeId]) const { nativeChatTranscriptIsLocalReadable, sshReconnectEnvironmentId, @@ -3219,8 +3184,6 @@ function TerminalPane( canToggleNativeChat={activePaneCanToggleChat} isChatViewMode={activePaneIsChatLeaf} onToggleNativeChat={handleToggleNativeChat} - canReturnStructuredSession={structuredHandoff?.status.owner === 'tui'} - onReturnStructuredSession={returnStructuredSession} canContinueAgentSessionInNewSession={activePaneCanContinueInNewSession} onContinueAgentSessionInNewSession={(pane) => contextMenu.runForPane(pane.id, contextMenu.onContinueAgentSessionInNewSession) diff --git a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx index d78336c17cc..a9ed8249179 100644 --- a/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx +++ b/src/renderer/src/components/terminal-pane/TerminalPaneHeaderOverlay.tsx @@ -8,7 +8,6 @@ import { } from 'lucide-react' import type { ManagedPane, PaneManager } from '@/lib/pane-manager/pane-manager' import { Button } from '@/components/ui/button' -import { StructuredAgentSessionTerminalReturnButton } from './StructuredAgentSessionTerminalReturnButton' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { translate } from '@/i18n/i18n' import { WORKSPACE_FILE_PATH_MIME, WORKSPACE_FILE_PATHS_MIME } from '@/lib/workspace-file-drag' @@ -51,8 +50,6 @@ type TerminalPaneHeaderOverlayProps = { isChatViewMode?: boolean /** Flip the active pane between the terminal and the native chat view. */ onToggleNativeChat?: () => void - canReturnStructuredSession?: boolean - onReturnStructuredSession?: () => void canContinueAgentSessionInNewSession?: boolean onContinueAgentSessionInNewSession?: (pane: ManagedPane) => void onSplitPane: (pane: ManagedPane, direction: 'vertical' | 'horizontal') => void @@ -91,8 +88,6 @@ export default function TerminalPaneHeaderOverlay({ canToggleNativeChat, isChatViewMode, onToggleNativeChat, - canReturnStructuredSession, - onReturnStructuredSession, canContinueAgentSessionInNewSession, onContinueAgentSessionInNewSession, onSplitPane, @@ -249,10 +244,6 @@ export default function TerminalPaneHeaderOverlay({ ) : null}
- {canContinueAgentSessionInNewSession && isActivePane ? ( diff --git a/src/renderer/src/runtime/structured-agent-session-handoff-store.ts b/src/renderer/src/runtime/structured-agent-session-handoff-store.ts deleted file mode 100644 index 8800f349802..00000000000 --- a/src/renderer/src/runtime/structured-agent-session-handoff-store.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { useSyncExternalStore } from 'react' -import type { AgentSessionHandoffStatus } from '../../../shared/agent-session-wire' - -export type TerminalStructuredHandoff = { - sessionId: string - fence: number - status: AgentSessionHandoffStatus -} - -const byTerminalTabId = new Map() -const listeners = new Set<() => void>() - -export function publishStructuredHandoff(input: TerminalStructuredHandoff): void { - for (const [tabId, current] of byTerminalTabId) { - if (current.sessionId === input.sessionId && tabId !== input.status.terminal?.tabId) { - byTerminalTabId.delete(tabId) - } - } - if (input.status.terminal) { - byTerminalTabId.set(input.status.terminal.tabId, input) - } - for (const listener of listeners) { - listener() - } -} - -export function clearStructuredHandoff(sessionId: string): void { - let changed = false - for (const [tabId, current] of byTerminalTabId) { - if (current.sessionId === sessionId) { - byTerminalTabId.delete(tabId) - changed = true - } - } - if (changed) { - for (const listener of listeners) { - listener() - } - } -} - -export function useTerminalStructuredHandoff(tabId: string): TerminalStructuredHandoff | null { - return useSyncExternalStore( - (listener) => { - listeners.add(listener) - return () => listeners.delete(listener) - }, - () => byTerminalTabId.get(tabId) ?? null - ) -} diff --git a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts index e41c2f0364a..47b981707c0 100644 --- a/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts +++ b/tests/e2e/cross-version-wire/cross-version-agent-session-wire.unit.test.ts @@ -46,7 +46,6 @@ const STRUCTURED_CALLS: { method: string; hostMethod: string | null }[] = [ { method: 'agentSession.respondToApproval', hostMethod: 'respondToPrompt' }, { method: 'agentSession.respondToQuestion', hostMethod: 'respondToPrompt' }, { method: 'agentSession.setOption', hostMethod: 'setOption' }, - { method: 'agentSession.requestHandoff', hostMethod: 'requestHandoff' }, { method: 'agentSession.handoffStatus', hostMethod: 'handoffStatus' }, { method: 'agentSession.options', hostMethod: 'readOptions' }, { method: 'agentSession.history', hostMethod: 'history' }, @@ -153,10 +152,6 @@ function paramsFor(method: string): unknown { const fields = { key: 'model', value: 'gpt-5' } return { envelope: envelope({ method, fields, fence }), ...fields } } - case 'agentSession.requestHandoff': { - const fields = { direction: 'to-tui', mode: 'now', action: 'start' } - return { envelope: envelope({ method, fields, fence }), ...fields } - } case 'agentSession.history': return { sessionId: SESSION, direction: 'tail' } default: