fix(x/renderer): enable suspended-turn cards; unify active-tab state for both chat views

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 <noreply@anthropic.com>
This commit is contained in:
Ramnique Singh 2026-07-02 11:51:31 +05:30
parent 58b016ba99
commit a58493b545
2 changed files with 48 additions and 41 deletions

View file

@ -949,6 +949,18 @@ function App() {
const streamingBuffersRef = useRef<Map<string, { assistant: string }>>(new Map()) const streamingBuffersRef = useRef<Map<string, { assistant: string }>>(new Map())
const [isStopping, setIsStopping] = useState(false) const [isStopping, setIsStopping] = useState(false)
const [, setStopClickedAt] = useState<number | null>(null) const [, setStopClickedAt] = useState<number | null>(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<ConversationItem[]>(() => (
sessionChat.error
? [{ id: 'session-load-error', kind: 'error', message: `Failed to load chat: ${sessionChat.error}`, timestamp: 0 }]
: []
), [sessionChat.error])
const [agentId] = useState<string>('copilot') const [agentId] = useState<string>('copilot')
const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined) const [presetMessage, setPresetMessage] = useState<string | undefined>(undefined)
@ -2545,7 +2557,7 @@ function App() {
codeMode?: 'claude' | 'codex', codeMode?: 'claude' | 'codex',
permissionMode?: PermissionMode, permissionMode?: PermissionMode,
) => { ) => {
if (isProcessing) return if (activeIsProcessing) return
const submitTabId = activeChatTabIdRef.current const submitTabId = activeChatTabIdRef.current
const { text } = message const { text } = message
@ -2728,23 +2740,6 @@ function App() {
handlePromptSubmitRef.current?.({ text: `${name} connected successfully.`, files: [] }) handlePromptSubmitRef.current?.({ text: `${name} connected successfully.`, files: [] })
}, []) }, [])
// A failed session load must be visible, not a blank chat.
const sessionLoadErrorItems = React.useMemo<ConversationItem[]>(() => (
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. // The composer's stop state clears when the active turn settles.
useEffect(() => { useEffect(() => {
if (sessionChat.chatState && !sessionChat.chatState.isProcessing) { if (sessionChat.chatState && !sessionChat.chatState.isProcessing) {
@ -5443,16 +5438,24 @@ function App() {
return null return null
} }
const activeChatTabState = React.useMemo<ChatTabViewState>(() => ({ // The active chat's view state, backed by the sessions hook (legacy
runId, // standalone states remain only as the pre-load fallback until stage 7).
conversation, const activeChatTabState = React.useMemo<ChatTabViewState>(() => (
currentAssistantMessage, sessionChat.chatState
pendingAskHumanRequests, ? { runId, ...sessionChat.chatState }
allPermissionRequests, : {
permissionResponses, runId,
autoPermissionDecisions, conversation: sessionLoadErrorItems.length > 0 ? sessionLoadErrorItems : conversation,
}), [ currentAssistantMessage,
pendingAskHumanRequests,
allPermissionRequests,
permissionResponses,
autoPermissionDecisions,
}
), [
runId, runId,
sessionChat.chatState,
sessionLoadErrorItems,
conversation, conversation,
currentAssistantMessage, currentAssistantMessage,
pendingAskHumanRequests, pendingAskHumanRequests,
@ -5465,6 +5468,10 @@ function App() {
if (tabId === activeChatTabId) return activeChatTabState if (tabId === activeChatTabId) return activeChatTabState
return chatViewStateByTab[tabId] ?? emptyChatTabState return chatViewStateByTab[tabId] ?? emptyChatTabState
}, [activeChatTabId, activeChatTabState, chatViewStateByTab, emptyChatTabState]) }, [activeChatTabId, activeChatTabState, chatViewStateByTab, emptyChatTabState])
const chatTabStatesForRender = React.useMemo(() => ({
...chatViewStateByTab,
[activeChatTabId]: activeChatTabState,
}), [chatViewStateByTab, activeChatTabId, activeChatTabState])
const selectedTask = selectedBackgroundTask const selectedTask = selectedBackgroundTask
? backgroundTasks.find(t => t.name === selectedBackgroundTask) ? backgroundTasks.find(t => t.name === selectedBackgroundTask)
: null : null
@ -6162,7 +6169,7 @@ function App() {
onApproveSession={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')} onApproveSession={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')}
onApproveAlways={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')} onApproveAlways={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')}
onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} onDeny={() => handlePermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')}
isProcessing={isActive && isProcessing} isProcessing={isActive && activeIsThinking}
response={response} response={response}
/> />
)} )}
@ -6180,7 +6187,7 @@ function App() {
query={request.query} query={request.query}
options={request.options} options={request.options}
onResponse={(response) => handleAskHumanResponse(request.toolCallId, request.subflow, response)} onResponse={(response) => handleAskHumanResponse(request.toolCallId, request.subflow, response)}
isProcessing={isActive && isProcessing} isProcessing={isActive && activeIsThinking}
/> />
))} ))}
@ -6192,7 +6199,7 @@ function App() {
</Message> </Message>
)} )}
{isActive && isProcessing && !tabState.currentAssistantMessage && ( {isActive && activeIsThinking && !tabState.currentAssistantMessage && (
<Message from="assistant"> <Message from="assistant">
<MessageContent> <MessageContent>
<Shimmer duration={1}>Thinking...</Shimmer> <Shimmer duration={1}>Thinking...</Shimmer>
@ -6228,7 +6235,7 @@ function App() {
visibleFiles={visibleKnowledgeFiles} visibleFiles={visibleKnowledgeFiles}
onSubmit={handlePromptSubmit} onSubmit={handlePromptSubmit}
onStop={handleStop} onStop={handleStop}
isProcessing={isActive && isProcessing} isProcessing={isActive && activeIsProcessing}
isStopping={isActive && isStopping} isStopping={isActive && isStopping}
isActive={isActive} isActive={isActive}
presetMessage={isActive ? presetMessage : undefined} presetMessage={isActive ? presetMessage : undefined}
@ -6310,11 +6317,11 @@ function App() {
}} }}
onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })} onOpenChatHistory={() => void navigateToView({ type: 'chat-history' })}
onOpenFullScreen={toggleRightPaneMaximize} onOpenFullScreen={toggleRightPaneMaximize}
conversation={sessionChat.chatState?.conversation ?? (sessionLoadErrorItems.length > 0 ? sessionLoadErrorItems : conversation)} conversation={activeChatTabState.conversation}
currentAssistantMessage={sessionChat.chatState?.currentAssistantMessage ?? currentAssistantMessage} currentAssistantMessage={activeChatTabState.currentAssistantMessage}
chatTabStates={chatTabStatesForRender} chatTabStates={chatTabStatesForRender}
viewportAnchors={chatViewportAnchorByTab} viewportAnchors={chatViewportAnchorByTab}
isProcessing={sessionChat.chatState ? sessionChat.chatState.isProcessing : isProcessing} isProcessing={activeIsProcessing}
isStopping={isStopping} isStopping={isStopping}
onStop={handleStop} onStop={handleStop}
onSubmit={handlePromptSubmit} onSubmit={handlePromptSubmit}
@ -6345,11 +6352,11 @@ function App() {
? { title: activeCodeSession.session.title } ? { title: activeCodeSession.session.title }
: null : null
} }
pendingAskHumanRequests={sessionChat.chatState?.pendingAskHumanRequests ?? pendingAskHumanRequests} pendingAskHumanRequests={activeChatTabState.pendingAskHumanRequests}
allPermissionRequests={sessionChat.chatState?.allPermissionRequests ?? allPermissionRequests} allPermissionRequests={activeChatTabState.allPermissionRequests}
permissionResponses={sessionChat.chatState?.permissionResponses ?? permissionResponses} permissionResponses={activeChatTabState.permissionResponses}
autoPermissionDecisions={sessionChat.chatState?.autoPermissionDecisions ?? autoPermissionDecisions} autoPermissionDecisions={activeChatTabState.autoPermissionDecisions}
isThinking={sessionChat.chatState?.isThinking} isThinking={activeIsThinking}
onPermissionResponse={handlePermissionResponse} onPermissionResponse={handlePermissionResponse}
onAskHumanResponse={handleAskHumanResponse} onAskHumanResponse={handleAskHumanResponse}
isToolOpenForTab={isToolOpenForTab} isToolOpenForTab={isToolOpenForTab}

View file

@ -730,7 +730,7 @@ export function ChatSidebar({
onApproveSession={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')} onApproveSession={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'session')}
onApproveAlways={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')} onApproveAlways={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'approve', 'always')}
onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')} onDeny={() => onPermissionResponse(permRequest.toolCall.toolCallId, permRequest.subflow, 'deny')}
isProcessing={isActive && isProcessing} isProcessing={isActive && (isThinking ?? isProcessing)}
response={response} response={response}
/> />
)} )}
@ -747,7 +747,7 @@ export function ChatSidebar({
key={request.toolCallId} key={request.toolCallId}
query={request.query} query={request.query}
onResponse={(response) => onAskHumanResponse(request.toolCallId, request.subflow, response)} onResponse={(response) => onAskHumanResponse(request.toolCallId, request.subflow, response)}
isProcessing={isActive && isProcessing} isProcessing={isActive && (isThinking ?? isProcessing)}
/> />
))} ))}