source dump of claude code
at main 229 lines 7.5 kB view raw
1import { useCallback, useEffect, useMemo, useRef } from 'react' 2import type { ToolUseConfirm } from '../components/permissions/PermissionRequest.js' 3import type { RemotePermissionResponse } from '../remote/RemoteSessionManager.js' 4import { 5 createSyntheticAssistantMessage, 6 createToolStub, 7} from '../remote/remotePermissionBridge.js' 8import { 9 convertSDKMessage, 10 isSessionEndMessage, 11} from '../remote/sdkMessageAdapter.js' 12import { 13 type DirectConnectConfig, 14 DirectConnectSessionManager, 15} from '../server/directConnectManager.js' 16import type { Tool } from '../Tool.js' 17import { findToolByName } from '../Tool.js' 18import type { Message as MessageType } from '../types/message.js' 19import type { PermissionAskDecision } from '../types/permissions.js' 20import { logForDebugging } from '../utils/debug.js' 21import { gracefulShutdown } from '../utils/gracefulShutdown.js' 22import type { RemoteMessageContent } from '../utils/teleport/api.js' 23 24type UseDirectConnectResult = { 25 isRemoteMode: boolean 26 sendMessage: (content: RemoteMessageContent) => Promise<boolean> 27 cancelRequest: () => void 28 disconnect: () => void 29} 30 31type UseDirectConnectProps = { 32 config: DirectConnectConfig | undefined 33 setMessages: React.Dispatch<React.SetStateAction<MessageType[]>> 34 setIsLoading: (loading: boolean) => void 35 setToolUseConfirmQueue: React.Dispatch<React.SetStateAction<ToolUseConfirm[]>> 36 tools: Tool[] 37} 38 39export function useDirectConnect({ 40 config, 41 setMessages, 42 setIsLoading, 43 setToolUseConfirmQueue, 44 tools, 45}: UseDirectConnectProps): UseDirectConnectResult { 46 const isRemoteMode = !!config 47 48 const managerRef = useRef<DirectConnectSessionManager | null>(null) 49 const hasReceivedInitRef = useRef(false) 50 const isConnectedRef = useRef(false) 51 52 // Keep a ref to tools so the WebSocket callback doesn't go stale 53 const toolsRef = useRef(tools) 54 useEffect(() => { 55 toolsRef.current = tools 56 }, [tools]) 57 58 useEffect(() => { 59 if (!config) { 60 return 61 } 62 63 hasReceivedInitRef.current = false 64 logForDebugging(`[useDirectConnect] Connecting to ${config.wsUrl}`) 65 66 const manager = new DirectConnectSessionManager(config, { 67 onMessage: sdkMessage => { 68 if (isSessionEndMessage(sdkMessage)) { 69 setIsLoading(false) 70 } 71 72 // Skip duplicate init messages (server sends one per turn) 73 if (sdkMessage.type === 'system' && sdkMessage.subtype === 'init') { 74 if (hasReceivedInitRef.current) { 75 return 76 } 77 hasReceivedInitRef.current = true 78 } 79 80 const converted = convertSDKMessage(sdkMessage, { 81 convertToolResults: true, 82 }) 83 if (converted.type === 'message') { 84 setMessages(prev => [...prev, converted.message]) 85 } 86 }, 87 onPermissionRequest: (request, requestId) => { 88 logForDebugging( 89 `[useDirectConnect] Permission request for tool: ${request.tool_name}`, 90 ) 91 92 const tool = 93 findToolByName(toolsRef.current, request.tool_name) ?? 94 createToolStub(request.tool_name) 95 96 const syntheticMessage = createSyntheticAssistantMessage( 97 request, 98 requestId, 99 ) 100 101 const permissionResult: PermissionAskDecision = { 102 behavior: 'ask', 103 message: 104 request.description ?? `${request.tool_name} requires permission`, 105 suggestions: request.permission_suggestions, 106 blockedPath: request.blocked_path, 107 } 108 109 const toolUseConfirm: ToolUseConfirm = { 110 assistantMessage: syntheticMessage, 111 tool, 112 description: 113 request.description ?? `${request.tool_name} requires permission`, 114 input: request.input, 115 toolUseContext: {} as ToolUseConfirm['toolUseContext'], 116 toolUseID: request.tool_use_id, 117 permissionResult, 118 permissionPromptStartTimeMs: Date.now(), 119 onUserInteraction() { 120 // No-op for remote 121 }, 122 onAbort() { 123 const response: RemotePermissionResponse = { 124 behavior: 'deny', 125 message: 'User aborted', 126 } 127 manager.respondToPermissionRequest(requestId, response) 128 setToolUseConfirmQueue(queue => 129 queue.filter(item => item.toolUseID !== request.tool_use_id), 130 ) 131 }, 132 onAllow(updatedInput, _permissionUpdates, _feedback) { 133 const response: RemotePermissionResponse = { 134 behavior: 'allow', 135 updatedInput, 136 } 137 manager.respondToPermissionRequest(requestId, response) 138 setToolUseConfirmQueue(queue => 139 queue.filter(item => item.toolUseID !== request.tool_use_id), 140 ) 141 setIsLoading(true) 142 }, 143 onReject(feedback?: string) { 144 const response: RemotePermissionResponse = { 145 behavior: 'deny', 146 message: feedback ?? 'User denied permission', 147 } 148 manager.respondToPermissionRequest(requestId, response) 149 setToolUseConfirmQueue(queue => 150 queue.filter(item => item.toolUseID !== request.tool_use_id), 151 ) 152 }, 153 async recheckPermission() { 154 // No-op for remote 155 }, 156 } 157 158 setToolUseConfirmQueue(queue => [...queue, toolUseConfirm]) 159 setIsLoading(false) 160 }, 161 onConnected: () => { 162 logForDebugging('[useDirectConnect] Connected') 163 isConnectedRef.current = true 164 }, 165 onDisconnected: () => { 166 logForDebugging('[useDirectConnect] Disconnected') 167 if (!isConnectedRef.current) { 168 // Never connected — connection failure (e.g. auth rejected) 169 process.stderr.write( 170 `\nFailed to connect to server at ${config.wsUrl}\n`, 171 ) 172 } else { 173 // Was connected then lost — server process exited or network dropped 174 process.stderr.write('\nServer disconnected.\n') 175 } 176 isConnectedRef.current = false 177 void gracefulShutdown(1) 178 setIsLoading(false) 179 }, 180 onError: error => { 181 logForDebugging(`[useDirectConnect] Error: ${error.message}`) 182 }, 183 }) 184 185 managerRef.current = manager 186 manager.connect() 187 188 return () => { 189 logForDebugging('[useDirectConnect] Cleanup - disconnecting') 190 manager.disconnect() 191 managerRef.current = null 192 } 193 }, [config, setMessages, setIsLoading, setToolUseConfirmQueue]) 194 195 const sendMessage = useCallback( 196 async (content: RemoteMessageContent): Promise<boolean> => { 197 const manager = managerRef.current 198 if (!manager) { 199 return false 200 } 201 202 setIsLoading(true) 203 204 return manager.sendMessage(content) 205 }, 206 [setIsLoading], 207 ) 208 209 // Cancel the current request 210 const cancelRequest = useCallback(() => { 211 // Send interrupt signal to the server 212 managerRef.current?.sendInterrupt() 213 214 setIsLoading(false) 215 }, [setIsLoading]) 216 217 const disconnect = useCallback(() => { 218 managerRef.current?.disconnect() 219 managerRef.current = null 220 isConnectedRef.current = false 221 }, []) 222 223 // Same stability concern as useRemoteSession — memoize so consumers 224 // that depend on the result object don't see a fresh reference per render. 225 return useMemo( 226 () => ({ isRemoteMode, sendMessage, cancelRequest, disconnect }), 227 [isRemoteMode, sendMessage, cancelRequest, disconnect], 228 ) 229}