diff --git a/frontend/src/app/pages/AgentChat/ChatInput.tsx b/frontend/src/app/pages/AgentChat/ChatInput.tsx index f9f77918..701105b8 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput.tsx @@ -149,12 +149,17 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, contextPaths, sessionFrameworkOverhead, }); if (block) { - // Auto-compact instead of prompting. Conversation history is the only - // overflow source we can shrink without losing user content — files were - // already auto-shrunk above, the prompt itself is the message the user - // just wrote, MCPs are framework. So if we're over, hit /compact, capture - // the send intent, and let the next-message effect fire it after the - // server-side compaction acks. User did nothing; problem solved silently. + if (block.kind === 'too_long') { + // This one message is too big to send even with zero history, so + // compaction can't save it. Hard-block and tell the user plainly; they + // shorten it and the block clears on the next send attempt. Don't fire + // /compact (pointless) and don't queue a retry (it'd just re-block). + pendingSendRef.current = null; + setSendBlock(block); + 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 { @@ -164,14 +169,14 @@ const ChatInput = forwardRef(({ onSend, disabled, mode, await fetch(`${API_BASE}/agents/sessions/${sessionId}/compact`, { method: 'POST', headers }); } catch (err) { console.error('[auto-compact] failed:', err); pendingSendRef.current = null; } } - // Briefly flash the banner as a status (not a prompt) so the user sees - // something happened. Auto-clear after 2s; in 99% of cases the auto-retry - // send has already fired by then. setSendBlock(block); setTimeout(() => setSendBlock(null), 2000); return; } + // Fits now (e.g. user shortened a too-long message): clear any lingering block banner. + setSendBlock(null); + onboardingBus.emit('chat:message_sent'); if (window.location.hash.includes('/apps/')) { onboardingBus.emit('app:generation_started'); diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts index 28f76581..dfc2dd90 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts @@ -17,6 +17,9 @@ function shrinkThreshold(modelCtx: number): number { } export type SendBlock = null | { + // 'compacting' = history overflow, auto-compact can fix it. + // 'too_long' = this single message exceeds the window on its own; hard block. + kind: 'compacting' | 'too_long'; estimate: number; window: number; history: number; diff --git a/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts b/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts index e6c82776..34288810 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/sendHelpers.ts @@ -35,7 +35,16 @@ export function computeSendBlock({ trimmed, currentModelCtx, historyUsed, contex for (const cp of contextPaths) { if ((cp.tokens || 0) > (largest?.tokens || 0)) largest = { path: cp.path, tokens: cp.tokens || 0 }; } + // Distinguish "history is the culprit" (compaction can fix it) from "this one + // message is too big on its own" (compaction can't help: dropping all prior + // turns still leaves framework+files+prompt over the window). The latter only + // happens with a giant pasted prompt, since attached files auto-shrink on + // attach. We surface that as a hard block instead of a doomed compact loop. + const nonHistory = framework + filesSum + promptTokens + systemTokens; + const kind: 'compacting' | 'too_long' = + nonHistory > Math.floor(win * 0.95) ? 'too_long' : 'compacting'; return { + kind, estimate, window: win, history, system: systemTokens, framework, files: filesSum, prompt: promptTokens, largestFile: largest, diff --git a/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx b/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx index 0666c653..01fa2dc8 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/view/SendBlockBanner.tsx @@ -9,9 +9,26 @@ interface Props { c: ClaudeTokens; } -/** Status indicator, not a prompt. Auto-compact already fired from handleSend; this - * just lets the user know we're freeing up space so the send doesn't look frozen. */ +/** 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). */ 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. + + + ); + } return (