[eric] ux: if one message is too long to ever fit, block it with a plain 'shorten or split it' note instead of a pointless auto-compact loop

This commit is contained in:
eric
2026-06-01 08:43:07 -07:00
parent 5fdbc3ab79
commit 36720b930b
4 changed files with 45 additions and 11 deletions
+14 -9
View File
@@ -149,12 +149,17 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ 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<ChatInputHandle, Props>(({ 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');
@@ -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;
@@ -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,
@@ -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<Props> = ({ sendBlock, c }) => {
if (sendBlock.kind === 'too_long') {
return (
<Box sx={{
mx: 1.5, mt: 1, mb: 0.5, px: 2, py: 1.25,
borderRadius: '12px',
bgcolor: c.bg.surface,
border: `1px solid ${c.border.medium}`,
}}>
<Typography sx={{ fontSize: '0.88rem', color: c.text.primary, lineHeight: 1.45 }}>
That message is too long to send. Try shortening it or splitting it into a few smaller ones.
</Typography>
</Box>
);
}
return (
<Box sx={{
mx: 1.5, mt: 1, mb: 0.5, px: 2, py: 1.25,