diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx index d3131577f90..fc01d132694 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.test.tsx @@ -86,7 +86,9 @@ vi.mock('./use-structured-agent-session', async () => { isWorking: false, isMonitoringBackgroundTasks: mocks.monitoringBackgroundTasks, supportsBackgroundTaskStop: mocks.supportsBackgroundTaskStop, - supportsBackgroundTaskStopAll: mocks.supportsBackgroundTaskStopAll, + backgroundTasksView: { + supportsBackgroundTaskStopAll: mocks.supportsBackgroundTaskStopAll + }, backgroundTasks: mocks.backgroundTasks, turnId: null, cancel: vi.fn(), diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts b/src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts index 6f071e1a16b..fb1e3d4a7e7 100644 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts +++ b/src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts @@ -7,7 +7,11 @@ import { useCallback, useRef, useState } from 'react' import * as conversationCommands from './structured-conversation-command-send' -import type { AgentSessionMutationResult } from '../../../../shared/agent-session-wire' +import type { + AgentSessionMutationResult, + AgentSessionWireRefusal +} from '../../../../shared/agent-session-wire' +import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code' import { agentSessionRefusalOperationState } from '../../../../shared/agent-session-refusal-retry' import { structuredAgentSessionPayloadFingerprint } from '../../../../shared/structured-agent-session-mutation' import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client' @@ -18,7 +22,8 @@ export type StructuredAgentSessionMutate = ( method: string, fingerprintMethod: string, fields: Record, - operationIdOverride?: string | null + operationIdOverride?: string | null, + onFailure?: (refusal?: AgentSessionWireRefusal) => void ) => Promise export function useStructuredAgentSessionMutate(args: { @@ -37,7 +42,8 @@ export function useStructuredAgentSessionMutate(args: { method: string, fingerprintMethod: string, fields: Record, - operationIdOverride?: string | null + operationIdOverride?: string | null, + onFailure?: (refusal?: AgentSessionWireRefusal) => void ): Promise => { if (stateRef.current.fence === null) { return null @@ -63,7 +69,17 @@ export function useStructuredAgentSessionMutate(args: { ...fields }) } catch (error) { - if (stateRef.current.fence === targetFence) { + if (onFailure) { + onFailure( + hasRuntimeRpcErrorCode(error, 'method_not_found') + ? { + code: 'structured_agent_session_unsupported', + message: '', + rewindReason: 'unsupported' + } + : undefined + ) + } else if (stateRef.current.fence === targetFence) { setWriteError(error instanceof Error ? error.message : 'Request was not sent') } return null @@ -75,12 +91,14 @@ export function useStructuredAgentSessionMutate(args: { ) { operationIds.current.delete(key) } - if (stateRef.current.fence === targetFence) { + if (onFailure) { + onFailure(result.refusal) + } else if (stateRef.current.fence === targetFence) { setWriteError(result.refusal.message) } return null } - if (stateRef.current.fence !== targetFence) { + if (stateRef.current.fence !== targetFence && fingerprintMethod !== 'agentSession.rewind') { return null } if (!conversationCommands.isUnconfirmedConversationCommand(fingerprintMethod, result.value)) { diff --git a/src/renderer/src/components/native-chat/use-structured-agent-session-mutation.ts b/src/renderer/src/components/native-chat/use-structured-agent-session-mutation.ts deleted file mode 100644 index d8b6b55c6a6..00000000000 --- a/src/renderer/src/components/native-chat/use-structured-agent-session-mutation.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { translate } from '@/i18n/i18n' -import { useCallback, useEffect, useRef, useState } from 'react' -import type { - AgentSessionMutationResult, - AgentSessionWireRefusal -} from '../../../../shared/agent-session-wire' -import { hasRuntimeRpcErrorCode } from '../../../../shared/runtime-rpc-error-code' -import { agentSessionRefusalOperationState } from '../../../../shared/agent-session-refusal-retry' -import { structuredAgentSessionPayloadFingerprint } from '../../../../shared/structured-agent-session-mutation' -import type { RuntimeClientTarget } from '@/runtime/runtime-rpc-client' -import { callStructuredAgentSession } from '@/runtime/structured-agent-session-client' -import { structuredSessionOperationId } from './use-structured-agent-session-outbox' -import * as conversationCommands from './structured-conversation-command-send' - -export function useStructuredAgentSessionMutation( - sessionId: string, - target: RuntimeClientTarget, - fence: number | null -) { - const stateRef = useRef({ fence }) - const [writeError, setWriteError] = useState(null) - const operationIds = useRef(new Map()) - useEffect(() => { - stateRef.current = { fence } - }, [fence]) - const mutate = useCallback( - async ( - method: string, - fingerprintMethod: string, - fields: Record, - operationIdOverride?: string | null, - onFailure?: (refusal?: AgentSessionWireRefusal) => void - ): Promise => { - if (stateRef.current.fence === null) { - return null - } - const targetFence = stateRef.current.fence - const key = `${sessionId}:${fingerprintMethod}:${JSON.stringify(fields)}` - const clientOperationId = - operationIdOverride ?? operationIds.current.get(key) ?? structuredSessionOperationId() - operationIds.current.set(key, clientOperationId) - let result: AgentSessionMutationResult - try { - result = await callStructuredAgentSession>(target, method, { - envelope: { - sessionId, - clientOperationId, - expectedRuntimeFence: targetFence, - payloadFingerprint: structuredAgentSessionPayloadFingerprint({ - method: fingerprintMethod, - sessionId, - fields - }) - }, - ...fields - }) - } catch (error) { - if (onFailure) { - onFailure( - hasRuntimeRpcErrorCode(error, 'method_not_found') - ? { - code: 'structured_agent_session_unsupported', - message: '', - rewindReason: 'unsupported' - } - : undefined - ) - } else if (stateRef.current.fence === targetFence) { - setWriteError( - error instanceof Error - ? error.message - : translate('components.native-chat.requestNotSent', 'Request was not sent') - ) - } - return null - } - if (!result.ok) { - if ( - agentSessionRefusalOperationState(fingerprintMethod, result.refusal.code) === - 'settled-rejected' - ) { - operationIds.current.delete(key) - } - if (onFailure) { - onFailure(result.refusal) - } else if (stateRef.current.fence === targetFence) { - setWriteError(result.refusal.message) - } - return null - } - if (stateRef.current.fence !== targetFence && fingerprintMethod !== 'agentSession.rewind') { - return null - } - if (!conversationCommands.isUnconfirmedConversationCommand(fingerprintMethod, result.value)) { - operationIds.current.delete(key) - } - setWriteError(null) - return result.value - }, - [sessionId, target] - ) - - return { mutate, writeError } -} 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 82050ea811b..eaebfd30ec3 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 @@ -191,31 +191,44 @@ export function useStructuredAgentSession(args: { ) const prompts = pendingStructuredSessionPrompts(state.items) - const rewind = useNativeChatRewind({ - sessionId, - hostBlockedReason: summary?.rewindBlockedReason, - state, - support: - conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence - ? conversationSupport.rewind - : undefined, - supportResolved: - conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence, - blocked: Boolean( - turnId || - prompts.length || - isMonitoringBackgroundTasks || - outboxController.outbox.length || - commandPending.current - ), - send: (fields) => - mutate( - 'agentSession.rewind', - 'agentSession.rewind', - fields, - undefined - ) - }) + const rewindSupportResolved = + conversationSupport?.sessionId === sessionId && conversationSupport.fence === state.fence + const rewindSupport = rewindSupportResolved ? conversationSupport.rewind : undefined + const rewindBlocked = Boolean( + turnId || + prompts.length || + isMonitoringBackgroundTasks || + outboxController.outbox.length || + commandPending.current + ) + const rewindInput = useMemo[0]>( + () => ({ + sessionId, + hostBlockedReason: summary?.rewindBlockedReason, + state, + support: rewindSupport, + supportResolved: rewindSupportResolved, + blocked: rewindBlocked, + send: (fields, onFailure) => + mutate( + 'agentSession.rewind', + 'agentSession.rewind', + fields, + undefined, + onFailure + ) + }), + [ + sessionId, + summary?.rewindBlockedReason, + state, + rewindSupport, + rewindSupportResolved, + rewindBlocked, + mutate + ] + ) + const rewind = useNativeChatRewind(rewindInput) const { outbox } = outboxController const messages = useStructuredAgentSessionMessages(state.items, outbox, state.submissions) return {