diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 8b2a32f4..6b820f70 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -949,6 +949,18 @@ function App() { const streamingBuffersRef = useRef>(new Map()) const [isStopping, setIsStopping] = useState(false) const [, setStopClickedAt] = useState(null) + // Sessions runtime: hook-derived activity for the ACTIVE chat. + // activeIsProcessing blocks the composer until the turn settles; + // activeIsThinking ("actively working") drives shimmers and disables + // permission/ask-human buttons — false while waiting on the user. + const activeIsProcessing = sessionChat.chatState?.isProcessing ?? isProcessing + const activeIsThinking = sessionChat.chatState ? sessionChat.chatState.isThinking : isProcessing + // A failed session load must be visible, not a blank chat. + const sessionLoadErrorItems = React.useMemo(() => ( + sessionChat.error + ? [{ id: 'session-load-error', kind: 'error', message: `Failed to load chat: ${sessionChat.error}`, timestamp: 0 }] + : [] + ), [sessionChat.error]) const [agentId] = useState('copilot') const [presetMessage, setPresetMessage] = useState(undefined) @@ -2545,7 +2557,7 @@ function App() { codeMode?: 'claude' | 'codex', permissionMode?: PermissionMode, ) => { - if (isProcessing) return + if (activeIsProcessing) return const submitTabId = activeChatTabIdRef.current const { text } = message @@ -2728,23 +2740,6 @@ function App() { handlePromptSubmitRef.current?.({ text: `${name} connected successfully.`, files: [] }) }, []) - // A failed session load must be visible, not a blank chat. - const sessionLoadErrorItems = React.useMemo(() => ( - sessionChat.error - ? [{ id: 'session-load-error', kind: 'error', message: `Failed to load chat: ${sessionChat.error}`, timestamp: 0 }] - : [] - ), [sessionChat.error]) - - // Active tab renders from the sessions hook; background tabs keep the - // legacy cached view state until stage 7 retires it. - const chatTabStatesForRender = React.useMemo(() => { - if (!sessionChat.chatState) return chatViewStateByTab - return { - ...chatViewStateByTab, - [activeChatTabId]: { runId, ...sessionChat.chatState }, - } - }, [chatViewStateByTab, sessionChat.chatState, activeChatTabId, runId]) - // The composer's stop state clears when the active turn settles. useEffect(() => { if (sessionChat.chatState && !sessionChat.chatState.isProcessing) { @@ -5443,16 +5438,24 @@ function App() { return null } - const activeChatTabState = React.useMemo(() => ({ - runId, - conversation, - currentAssistantMessage, - pendingAskHumanRequests, - allPermissionRequests, - permissionResponses, - autoPermissionDecisions, - }), [ + // The active chat's view state, backed by the sessions hook (legacy + // standalone states remain only as the pre-load fallback until stage 7). + const activeChatTabState = React.useMemo(() => ( + sessionChat.chatState + ? { runId, ...sessionChat.chatState } + : { + runId, + conversation: sessionLoadErrorItems.length > 0 ? sessionLoadErrorItems : conversation, + currentAssistantMessage, + pendingAskHumanRequests, + allPermissionRequests, + permissionResponses, + autoPermissionDecisions, + } + ), [ runId, + sessionChat.chatState, + sessionLoadErrorItems, conversation, currentAssistantMessage, pendingAskHumanRequests, @@ -5465,6 +5468,10 @@ function App() { if (tabId === activeChatTabId) return activeChatTabState return chatViewStateByTab[tabId] ?? emptyChatTabState }, [activeChatTabId, activeChatTabState, chatViewStateByTab, emptyChatTabState]) + const chatTabStatesForRender = React.useMemo(() => ({ + ...chatViewStateByTab, + [activeChatTabId]: activeChatTabState, + }), [chatViewStateByTab, activeChatTabId, activeChatTabState]) const selectedTask = selectedBackgroundTask ? backgroundTasks.find(t => t.name === selectedBackgroundTask) : null @@ -6162,7 +6169,7 @@ function App() { onApproveSession={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')} onApproveAlways={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')} onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} - isProcessing={isActive && isProcessing} + isProcessing={isActive && activeIsThinking} response={response} /> )} @@ -6180,7 +6187,7 @@ function App() { query={request.query} options={request.options} onResponse={(response) => handleAskHumanResponse(request.toolCallId, request.subflow, response)} - isProcessing={isActive && isProcessing} + isProcessing={isActive && activeIsThinking} /> ))} @@ -6192,7 +6199,7 @@ function App() { )} - {isActive && isProcessing && !tabState.currentAssistantMessage && ( + {isActive && activeIsThinking && !tabState.currentAssistantMessage && ( Thinking... @@ -6228,7 +6235,7 @@ function App() { visibleFiles={visibleKnowledgeFiles} onSubmit={handlePromptSubmit} onStop={handleStop} - isProcessing={isActive && isProcessing} + isProcessing={isActive && activeIsProcessing} isStopping={isActive && isStopping} isActive={isActive} presetMessage={isActive ? presetMessage : undefined} @@ -6310,11 +6317,11 @@ function App() { }} onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} onOpenFullScreen={toggleRightPaneMaximize} - conversation={sessionChat.chatState?.conversation ?? (sessionLoadErrorItems.length > 0 ? sessionLoadErrorItems : conversation)} - currentAssistantMessage={sessionChat.chatState?.currentAssistantMessage ?? currentAssistantMessage} + conversation={activeChatTabState.conversation} + currentAssistantMessage={activeChatTabState.currentAssistantMessage} chatTabStates={chatTabStatesForRender} viewportAnchors={chatViewportAnchorByTab} - isProcessing={sessionChat.chatState ? sessionChat.chatState.isProcessing : isProcessing} + isProcessing={activeIsProcessing} isStopping={isStopping} onStop={handleStop} onSubmit={handlePromptSubmit} @@ -6345,11 +6352,11 @@ function App() { ? { title: activeCodeSession.session.title } : null } - pendingAskHumanRequests={sessionChat.chatState?.pendingAskHumanRequests ?? pendingAskHumanRequests} - allPermissionRequests={sessionChat.chatState?.allPermissionRequests ?? allPermissionRequests} - permissionResponses={sessionChat.chatState?.permissionResponses ?? permissionResponses} - autoPermissionDecisions={sessionChat.chatState?.autoPermissionDecisions ?? autoPermissionDecisions} - isThinking={sessionChat.chatState?.isThinking} + pendingAskHumanRequests={activeChatTabState.pendingAskHumanRequests} + allPermissionRequests={activeChatTabState.allPermissionRequests} + permissionResponses={activeChatTabState.permissionResponses} + autoPermissionDecisions={activeChatTabState.autoPermissionDecisions} + isThinking={activeIsThinking} onPermissionResponse={handlePermissionResponse} onAskHumanResponse={handleAskHumanResponse} isToolOpenForTab={isToolOpenForTab} diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 976f9684..893f9f1a 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -730,7 +730,7 @@ export function ChatSidebar({ onApproveSession={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')} onApproveAlways={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')} onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} - isProcessing={isActive && isProcessing} + isProcessing={isActive && (isThinking ?? isProcessing)} response={response} /> )} @@ -747,7 +747,7 @@ export function ChatSidebar({ key={request.toolCallId} query={request.query} onResponse={(response) => onAskHumanResponse(request.toolCallId, request.subflow, response)} - isProcessing={isActive && isProcessing} + isProcessing={isActive && (isThinking ?? isProcessing)} /> ))}