diff --git a/mobile/src/session/use-mobile-native-chat-controller.ts b/mobile/src/session/use-mobile-native-chat-controller.ts index 7aafafad5b7..ff4fa08a225 100644 --- a/mobile/src/session/use-mobile-native-chat-controller.ts +++ b/mobile/src/session/use-mobile-native-chat-controller.ts @@ -3,20 +3,17 @@ import type { RpcClient } from '../transport/rpc-client' import type { ConnectionState } from '../transport/types' import type { MobileNativeChatTab } from './native-chat/mobile-native-chat-eligibility' import type { StructuredAgentSessionHostSupport } from './mobile-structured-agent-session-host-support' -import { useMobileNativeChatAskDismiss } from './native-chat/input/use-mobile-native-chat-ask-dismiss' import { useMobileNativeChatDrafts } from './native-chat/composer/use-mobile-native-chat-drafts' import { useMobileNativeChatWorkspaceCatalog } from './native-chat/composer/use-mobile-native-chat-workspace-catalog' import { useMobileNativeChatMessageSend } from './native-chat/transcript/use-mobile-native-chat-message-send' import { useMobileNativeChatSessionOptionController } from './native-chat/input/use-mobile-native-chat-session-option-controller' import { useMobileNativeChatSessionLane } from './use-mobile-native-chat-session-lane' import { useMobileStructuredNativeChatSendBridge } from './use-mobile-structured-native-chat-send-bridge' -import { useMobileNativeChatPrompts } from './native-chat/input/use-mobile-native-chat-prompts' -import { useNativeChatAcceptedAction } from './use-native-chat-action-outcomes' import { useMobileNativeChatTerminalTurn } from './use-mobile-native-chat-terminal-turn' import type { MobileNativeChatController } from './mobile-native-chat-controller-contract' import { useMobileBridgeChatPromptWrites } from './use-mobile-bridge-chat-prompt-writes' import { useMobileNativeChatActiveResolution } from './use-mobile-native-chat-active-resolution' -import { useMobileNativeChatPromptCards } from './native-chat/input/use-mobile-native-chat-prompt-cards' +import { useMobileNativeChatPromptController } from './use-mobile-native-chat-prompt-controller' import { mobileNativeChatScopeKey } from './native-chat/mobile-native-chat-scope-key' export type { MobileNativeChatController } from './mobile-native-chat-controller-contract' @@ -113,42 +110,12 @@ export function useMobileNativeChatController(args: { transcriptSettled: nativeChatSession.status === 'ready' }) - const { - permission: legacyNativeChatPermission, - ask: nativeChatAskPrompt, - waitStartedAt: nativeChatPromptStartedAt, - structuredAnswer - } = useMobileNativeChatPrompts({ - enabled: activeChatStructured || activeChatResolution != null, - structured: activeChatStructured ? structuredNativeChat : null, - status: nativeChatStatus, - messages: nativeChatSession.messages, - transcriptLoading: nativeChatSession.transcriptLoading - }) - // A never-read transcript cannot prove that a dismissed prompt cleared. - const nativeChatTranscriptSettled = - nativeChatSession.status === 'ready' || - (nativeChatSession.status === 'error' && nativeChatSession.messages.length > 0) - const promptScopeKey = mobileNativeChatScopeKey(hostId, worktreeId, activeSessionTabId) - const askDismissal = useMobileNativeChatAskDismiss({ - ask: nativeChatAskPrompt, - startedAt: nativeChatPromptStartedAt, - scopeKey: promptScopeKey, - sessionKey: activeChatSessionId, - observing: showNativeChat && (nativeChatAskPrompt != null || nativeChatTranscriptSettled) - }) - // Require both input lease and live connection; remembered-only agents may now be shells and must never receive chat writes. const inputSendable = activeChatStructured ? client != null && activeChatSessionId != null && connState === 'connected' : nativeChatInputLeaseReady && connState === 'connected' && activeChatAgentConfirmed - const { - answerAsk: bridgeAnswerAsk, - cancelAsk: handleNativeChatCancelAsk, - respondPermission: legacyHandleNativeChatRespondPermission, - stop: handleNativeChatStop - } = useMobileBridgeChatPromptWrites({ + const { stop: handleNativeChatStop, ...bridgePromptWrites } = useMobileBridgeChatPromptWrites({ client, enabled: inputSendable && !activeChatStructured, handleRef: activeHandleRef, @@ -159,12 +126,25 @@ export function useMobileNativeChatController(args: { onSendError }) + const { hasPromptCard, ...prompts } = useMobileNativeChatPromptController({ + enabled: activeChatStructured || activeChatResolution != null, + structured: activeChatStructured ? structuredNativeChat : null, + status: nativeChatStatus, + session: nativeChatSession, + scopeKey: mobileNativeChatScopeKey(hostId, worktreeId, activeSessionTabId), + sessionId: activeChatSessionId, + streamIdentity, + visible: showNativeChat, + terminal: bridgePromptWrites, + onSendResolved + }) + const terminalTurn = useMobileNativeChatTerminalTurn({ status: nativeChatStatus, resolution: activeChatResolution, session: activeChatStructured ? null : nativeChatSession, latestPendingId: chatPending.at(-1)?.id ?? null, - hasPromptCard: (legacyNativeChatPermission ?? nativeChatAskPrompt) != null, + hasPromptCard, scopeKey: streamIdentity, stop: handleNativeChatStop }) @@ -234,20 +214,6 @@ export function useMobileNativeChatController(args: { recordSessionOptionCommandRef.current = recordNativeChatSessionOptionCommand appendComposerTextRef.current = appendComposerText }, [appendComposerText, recordNativeChatSessionOptionCommand]) - const answerAsk = useNativeChatAcceptedAction(structuredAnswer ?? bridgeAnswerAsk, onSendResolved) - const cancelAsk = useNativeChatAcceptedAction(handleNativeChatCancelAsk, onSendResolved) - const promptCards = useMobileNativeChatPromptCards({ - terminal: { - permission: legacyNativeChatPermission, - waitStartedAt: nativeChatPromptStartedAt, - scopeKey: promptScopeKey, - sessionKey: streamIdentity, - observing: showNativeChat && !activeChatStructured && nativeChatStatus != null, - respondPermission: legacyHandleNativeChatRespondPermission - }, - structured: activeChatStructured ? structuredNativeChat : null, - onSendResolved - }) return { reportQueuedEditError: onSendError, @@ -286,14 +252,7 @@ export function useMobileNativeChatController(args: { nativeChatCommandRefusalCauses: structuredNativeChat.commandRefusalCauses, nativeChatCanStop: activeChatStructured ? structuredNativeChat.canStop : nativeChatAgentWorking, nativeChatStreamingText: terminalTurn.streamingText, - ...promptCards, - nativeChatAsk: askDismissal.showAsk || askDismissal.collapsedAsk ? nativeChatAskPrompt : null, - nativeChatAskKey: askDismissal.askKey, - dismissNativeChatAsk: askDismissal.dismissAsk, - collapseNativeChatAsk: askDismissal.collapseAsk, - nativeChatCollapsedPrompt: askDismissal.collapsedAsk ?? promptCards.nativeChatCollapsedPrompt, - handleNativeChatAnswerAsk: answerAsk, - handleNativeChatCancelAsk: cancelAsk, + ...prompts, handleNativeChatStop: activeChatStructured ? structuredNativeChat.cancel : terminalTurn.stop, nativeChatQueued: structuredNativeChat.queued, nativeChatBackgroundTasks: activeChatStructured ? structuredNativeChat.backgroundTasks : null, diff --git a/mobile/src/session/use-mobile-native-chat-prompt-controller.ts b/mobile/src/session/use-mobile-native-chat-prompt-controller.ts new file mode 100644 index 00000000000..6ec9315496c --- /dev/null +++ b/mobile/src/session/use-mobile-native-chat-prompt-controller.ts @@ -0,0 +1,109 @@ +import type { AgentStatusEntry } from '../../../src/shared/agent-status-types' +import type { MobileNativeChatController } from './mobile-native-chat-controller-contract' +import type { useMobileBridgeChatPromptWrites } from './use-mobile-bridge-chat-prompt-writes' +import { useMobileNativeChatAskDismiss } from './native-chat/input/use-mobile-native-chat-ask-dismiss' +import { useMobileNativeChatPromptCards } from './native-chat/input/use-mobile-native-chat-prompt-cards' +import { useMobileNativeChatPrompts } from './native-chat/input/use-mobile-native-chat-prompts' +import type { useMobileNativeChatSession } from './use-mobile-native-chat-session' +import type { useMobileStructuredAgentSession } from './use-mobile-structured-agent-session' +import { useNativeChatAcceptedAction } from './use-native-chat-action-outcomes' + +// Read the chat's ask and permission/question prompts and route their answers to the active lane. +export function useMobileNativeChatPromptController({ + enabled, + structured, + status, + session, + scopeKey, + sessionId, + streamIdentity, + visible, + terminal, + onSendResolved +}: { + enabled: boolean + structured: ReturnType | null + status: AgentStatusEntry | null | undefined + session: Pick< + ReturnType, + 'messages' | 'transcriptLoading' | 'status' + > + scopeKey: string | null + sessionId: string | null + streamIdentity: string + visible: boolean + /** Terminal-bridge writes; a structured chat answers through its own session. */ + terminal: Pick< + ReturnType, + 'answerAsk' | 'cancelAsk' | 'respondPermission' + > + onSendResolved: () => void +}): Pick< + MobileNativeChatController, + | 'nativeChatPermission' + | 'nativeChatQuestion' + | 'nativeChatPromptKey' + | 'handleNativeChatRespondPermission' + | 'handleNativeChatQuestionAnswer' + | 'handleNativeChatCancelPrompt' + | 'collapseNativeChatPrompt' + | 'nativeChatCollapsedPrompt' + | 'nativeChatAsk' + | 'nativeChatAskKey' + | 'dismissNativeChatAsk' + | 'collapseNativeChatAsk' + | 'handleNativeChatAnswerAsk' + | 'handleNativeChatCancelAsk' +> & { hasPromptCard: boolean } { + const { + permission: legacyNativeChatPermission, + waitStartedAt: nativeChatPromptStartedAt, + ask: nativeChatAskPrompt, + structuredAnswer + } = useMobileNativeChatPrompts({ + enabled, + structured, + status, + messages: session.messages, + transcriptLoading: session.transcriptLoading + }) + // A never-read transcript cannot prove that a dismissed prompt cleared. + const nativeChatTranscriptSettled = + session.status === 'ready' || (session.status === 'error' && session.messages.length > 0) + const askDismissal = useMobileNativeChatAskDismiss({ + ask: nativeChatAskPrompt, + startedAt: nativeChatPromptStartedAt, + scopeKey, + sessionKey: sessionId, + observing: visible && (nativeChatAskPrompt != null || nativeChatTranscriptSettled) + }) + const answerAsk = useNativeChatAcceptedAction( + structuredAnswer ?? terminal.answerAsk, + onSendResolved + ) + const cancelAsk = useNativeChatAcceptedAction(terminal.cancelAsk, onSendResolved) + const promptCards = useMobileNativeChatPromptCards({ + terminal: { + permission: legacyNativeChatPermission, + waitStartedAt: nativeChatPromptStartedAt, + scopeKey, + sessionKey: streamIdentity, + observing: visible && !structured && status != null, + respondPermission: terminal.respondPermission + }, + structured, + onSendResolved + }) + + return { + hasPromptCard: (legacyNativeChatPermission ?? nativeChatAskPrompt) != null, + ...promptCards, + nativeChatAsk: askDismissal.showAsk || askDismissal.collapsedAsk ? nativeChatAskPrompt : null, + nativeChatAskKey: askDismissal.askKey, + dismissNativeChatAsk: askDismissal.dismissAsk, + collapseNativeChatAsk: askDismissal.collapseAsk, + nativeChatCollapsedPrompt: askDismissal.collapsedAsk ?? promptCards.nativeChatCollapsedPrompt, + handleNativeChatAnswerAsk: answerAsk, + handleNativeChatCancelAsk: cancelAsk + } +}