ml-intern / frontend /src /components /SessionChat.tsx
akseljoonas's picture
akseljoonas HF Staff
feat: render cancelled tool state in UI after user interrupt
d2c1b12
raw
history blame
4.43 kB
/**
* Per-session chat component.
*
* Each session renders its own SessionChat. The hook (useAgentChat) always
* runs — processing events — but only the active session renders visible
* UI (MessageList + ChatInput).
*/
import { useCallback, useEffect, useState } from 'react';
import { useAgentChat } from '@/hooks/useAgentChat';
import { useAgentStore } from '@/store/agentStore';
import { useSessionStore } from '@/store/sessionStore';
import MessageList from '@/components/Chat/MessageList';
import ChatInput from '@/components/Chat/ChatInput';
import { apiFetch } from '@/utils/api';
import { logger } from '@/utils/logger';
interface SessionChatProps {
sessionId: string;
isActive: boolean;
onSessionDead: (sessionId: string) => void;
}
export default function SessionChat({ sessionId, isActive, onSessionDead }: SessionChatProps) {
const { isConnected, isProcessing, activityStatus, updateSession } = useAgentStore();
const { updateSessionTitle } = useSessionStore();
const [wasCancelled, setWasCancelled] = useState(false);
const { messages, sendMessage, stop, status, undoLastTurn, approveTools } = useAgentChat({
sessionId,
isActive,
onReady: () => logger.log(`Session ${sessionId} ready`),
onError: (error) => logger.error(`Session ${sessionId} error:`, error),
onSessionDead,
});
// When this session becomes active, restore its per-session state to the
// global flat fields. The per-session state map is kept up-to-date by
// side-channel callbacks even while the session is in the background.
useEffect(() => {
if (isActive) {
useAgentStore.getState().switchActiveSession(sessionId);
useAgentStore.getState().setConnected(true);
}
}, [isActive, sessionId]);
// Re-sync state when the browser tab regains focus (Chrome throttles
// timers in background tabs which can stall the AI SDK's update flushing).
// Fires for ALL sessions so background sessions also recover after sleep.
useEffect(() => {
const onVisible = () => {
if (document.visibilityState === 'visible' && isActive) {
useAgentStore.getState().switchActiveSession(sessionId);
}
};
document.addEventListener('visibilitychange', onVisible);
return () => document.removeEventListener('visibilitychange', onVisible);
}, [isActive, sessionId]);
// Wrap stop to track cancellation
const handleStop = useCallback(() => {
stop();
setWasCancelled(true);
}, [stop]);
// SDK status is the ground truth — if it's streaming/submitted, agent is busy
const sdkBusy = status === 'streaming' || status === 'submitted';
const busy = isProcessing || sdkBusy;
const handleSendMessage = useCallback(
async (text: string) => {
if (!text.trim() || busy) return;
setWasCancelled(false);
updateSession(sessionId, { isProcessing: true });
sendMessage({ text: text.trim(), metadata: { createdAt: new Date().toISOString() } });
// Auto-title the session from the first user message
const isFirstMessage = messages.filter((m) => m.role === 'user').length <= 1;
if (isFirstMessage) {
apiFetch('/api/title', {
method: 'POST',
body: JSON.stringify({ session_id: sessionId, text: text.trim() }),
})
.then((res) => res.json())
.then((data) => {
if (data.title) updateSessionTitle(sessionId, data.title);
})
.catch(() => {
const raw = text.trim();
updateSessionTitle(sessionId, raw.length > 40 ? raw.slice(0, 40) + '\u2026' : raw);
});
}
},
[sessionId, sendMessage, messages, updateSessionTitle, busy, updateSession],
);
// Don't render UI for background sessions — hooks still run
if (!isActive) return null;
return (
<>
<MessageList
messages={messages}
isProcessing={busy}
approveTools={approveTools}
onUndoLastTurn={undoLastTurn}
/>
<ChatInput
onSend={handleSendMessage}
onStop={handleStop}
isProcessing={busy}
disabled={!isConnected || activityStatus.type === 'waiting-approval'}
placeholder={
activityStatus.type === 'waiting-approval'
? 'Approve or reject pending tools first...'
: wasCancelled
? 'What should the agent do instead?'
: undefined
}
/>
</>
);
}