diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index 18888f8d..b0925caf 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -201,6 +201,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose const [showScrollButton, setShowScrollButton] = useState(false); const [showResumeBubble, setShowResumeBubble] = useState(false); const [awaitingResponse, setAwaitingResponse] = useState(false); + const [preSendActivityLabel, setPreSendActivityLabel] = useState(null); const [activatingMcp, setActivatingMcp] = useState(null); const [activateError, setActivateError] = useState(null); const [mode, setMode] = useState('agent'); @@ -385,6 +386,18 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose const streamingMessageId = useAppSelector((s) => id ? s.streaming.bySession[id]?.id ?? null : null); const hasStreaming = !!streamingMessageId; + useEffect(() => { + if ( + streamingMessageId || + session?.turn_label?.label || + session?.status === 'completed' || + session?.status === 'error' || + session?.status === 'stopped' + ) { + setPreSendActivityLabel(null); + } + }, [streamingMessageId, session?.turn_label?.label, session?.status]); + useEffect(() => { if (reconcileTimer.current) { clearTimeout(reconcileTimer.current); @@ -1422,10 +1435,10 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose /> )} - {(awaitingResponse || (session.status === 'running' && !streamingMessageId)) && ( + {(preSendActivityLabel || awaitingResponse || (session.status === 'running' && !streamingMessageId)) && ( @@ -1781,6 +1794,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose autoFocus={autoFocus} thinkingLevel={session?.thinking_level ?? 'auto'} onThinkingLevelChange={handleThinkingLevelChange} + onActivityLabelChange={setPreSendActivityLabel} /> diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index 701105b8..243a0c51 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -41,9 +41,10 @@ interface Props { queueLength?: number; thinkingLevel?: 'off' | 'low' | 'medium' | 'high' | 'auto'; onThinkingLevelChange?: (level: 'off' | 'low' | 'medium' | 'high' | 'auto') => void; + onActivityLabelChange?: (label: string | null) => void; } -const ChatInput = forwardRef(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange }, ref) => { +const ChatInput = forwardRef(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange, onActivityLabelChange }, ref) => { const c = useClaudeTokens(); const editorRef = useRef(null); const containerRef = useRef(null); @@ -73,6 +74,9 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, ); const { allModelOptions, currentModelCtx, pdfSupported, imageSupported } = useChatInputModel(model); + const compactionInFlightRef = useRef(false); + + useEffect(() => () => onActivityLabelChange?.(null), [onActivityLabelChange]); useEffect(() => { if (modesArr.length === 0) dispatch(fetchModes()); @@ -144,7 +148,8 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, if (!trimmed) return; const block = computeSendBlock({ - trimmed, currentModelCtx, + trimmed, + currentModelCtx, historyUsed: contextEstimate?.used ?? 0, contextPaths, sessionFrameworkOverhead, }); @@ -159,19 +164,21 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, return; } // kind === 'compacting': history is the overflow source, which we CAN - // shrink. Auto-compact, capture the send intent, flash a status banner. - if (sessionId) { - pendingSendRef.current = () => { handleSend(); }; - try { - const tok = (() => { try { return getAuthToken(); } catch { return ''; } })(); - const headers: Record = { 'Content-Type': 'application/json' }; - if (tok) headers['Authorization'] = `Bearer ${tok}`; - await fetch(`${API_BASE}/agents/sessions/${sessionId}/compact`, { method: 'POST', headers }); - } catch (err) { console.error('[auto-compact] failed:', err); pendingSendRef.current = null; } + // shrink. Auto-compact invisibly, then continue this same send. + if (!sessionId || compactionInFlightRef.current) return; + compactionInFlightRef.current = true; + setSendBlock(null); + onActivityLabelChange?.('Compacting memory'); + try { + const tok = (() => { try { return getAuthToken(); } catch { return ''; } })(); + const headers: Record = { 'Content-Type': 'application/json' }; + if (tok) headers['Authorization'] = `Bearer ${tok}`; + const resp = await fetch(`${API_BASE}/agents/sessions/${sessionId}/compact`, { method: 'POST', headers }); + if (!resp.ok) throw new Error(`compact failed: ${resp.status}`); + } catch (err) { + console.error('[auto-compact] failed:', err); } - setSendBlock(block); - setTimeout(() => setSendBlock(null), 2000); - return; + compactionInFlightRef.current = false; } // Fits now (e.g. user shortened a too-long message): clear any lingering block banner. @@ -227,7 +234,7 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, setAttachedSkills({}); setHasContent(false); elementSelection?.clearOwnerElements(ownerId); - }, [disabled, images, contextPaths, forcedTools, onSend, elementSelection, ownerId, summarizingPath, summarizingAll, oversizeQueue, pendingSendRef, sessionId, currentModelCtx, contextEstimate, sessionFrameworkOverhead, setSendBlock]); + }, [disabled, images, contextPaths, forcedTools, onSend, elementSelection, ownerId, summarizingPath, summarizingAll, oversizeQueue, pendingSendRef, sessionId, currentModelCtx, contextEstimate, sessionFrameworkOverhead, setSendBlock, onActivityLabelChange]); const { picker: editorPicker, setPicker, diff --git a/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx b/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx index 01fa2dc8..c0bc27a9 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx @@ -9,46 +9,18 @@ interface Props { c: ClaudeTokens; } -/** Two states: - * - 'compacting': auto-compact already fired; pulsing-dot status so the send - * doesn't look frozen while we free up room. - * - 'too_long': this single message can't fit no matter what; a plain, friendly - * block telling the user to shorten it (no jargon, no spinner). */ +/** Only hard-blocks render here. History compaction is shown as normal chat activity. */ export const SendBlockBanner: React.FC = ({ sendBlock, c }) => { - if (sendBlock.kind === 'too_long') { - return ( - - - That message is too long to send. Try shortening it or splitting it into a few smaller ones. - - - ); - } + if (sendBlock.kind !== 'too_long') return null; return ( - - Making room for your message + That message is too long to send. Try shortening it or splitting it into a few smaller ones. );