mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-12 21:02:17 +02:00
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:
parent
58b016ba99
commit
a58493b545
2 changed files with 48 additions and 41 deletions
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue