From a58493b545da33ef4413bcdaaed406f05007b5af Mon Sep 17 00:00:00 2001 From: Ramnique Singh <30795890+ramnique@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:51:31 +0530 Subject: [PATCH] fix(x/renderer): enable suspended-turn cards; unify active-tab state for both chat views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups from live testing: 1. Permission/ask-human buttons were disabled while a turn waited for input. PermissionRequest disables via isProcessing, whose old-runtime meaning was 'agent busy right now' (false while awaiting approval); the sessions meaning is 'turn not settled' (true while suspended). Cards in both chat views now take the isThinking flag — actively working disables, waiting on the user enables — while the composer and stop button stay on isProcessing. 2. The middle-pane (full-screen) chat rendered blank: App.tsx has its own conversation block whose activeChatTabState was still assembled from legacy standalone states. activeChatTabState is now the single hook-backed source of truth for the active tab; the sidebar props, the middle pane, the merged background-tab map, and the submit guard all derive from it (plus hook-driven activeIsProcessing/ activeIsThinking), so both views land on identical data. Co-Authored-By: Claude Fable 5 --- apps/x/apps/renderer/src/App.tsx | 85 ++++++++++--------- .../renderer/src/components/chat-sidebar.tsx | 4 +- 2 files changed, 48 insertions(+), 41 deletions(-) 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)} /> ))}