diff --git a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts index d8d6c06e..28f76581 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts +++ b/frontend/src/app/pages/AgentChat/ChatInput/hooks/useContextFiles.ts @@ -4,15 +4,16 @@ import { API_BASE, getAuthToken } from '@/shared/config'; import { ForcedToolGroup } from '../types'; import { basename } from '../helpers'; -// Only auto-shrink a file when it would eat 85%+ of the window on its own. Below -// that, send it NATIVELY to the model (base64 document block) the way claude.ai / +// Only auto-shrink a file when it literally won't fit (98%+ of the window on its +// own). Below that, send it NATIVELY (base64 document block) the way claude.ai / // OpenAI / Gemini do — the model reads the PDF server-side, instantly, no separate // summarize round-trip. The old 50% trigger was force-summarizing files that fit // fine, which is the entire reason our file flow felt 60s-slow vs their instant: we -// were doing pre-processing work the big providers simply don't do. Leaves ~15% for -// the conversation; if it grows past the window later, auto-compact handles it. +// were doing pre-processing work the big providers simply don't do. 98% (not 100%) +// leaves a sliver for the prompt itself so a barely-fitting file doesn't 4xx; if the +// conversation later grows past the window, auto-compact handles it. function shrinkThreshold(modelCtx: number): number { - return Math.floor(modelCtx * 0.85); + return Math.floor(modelCtx * 0.98); } export type SendBlock = null | { diff --git a/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ContextRing.tsx b/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ContextRing.tsx index 0eea0bda..1274b78e 100644 --- a/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ContextRing.tsx +++ b/frontend/src/app/pages/AgentChat/ChatInput/toolbar/ContextRing.tsx @@ -4,6 +4,21 @@ import Tooltip from '@mui/material/Tooltip'; import { formatTokenCount } from '../helpers'; export const ContextRing: React.FC<{ used: number; limit: number; accentColor: string; trackColor: string }> = ({ used, limit, accentColor, trackColor }) => { + // Track the previous fill so a DROP (compaction freed space) can play a brief + // "settle" cue: the ring eases down AND flashes once toward the track color, + // signaling "we just made room" without a loud banner. A rise just eases up. + const prevUsed = React.useRef(used); + const [justCompacted, setJustCompacted] = React.useState(false); + React.useEffect(() => { + if (used < prevUsed.current - 1) { + setJustCompacted(true); + const t = setTimeout(() => setJustCompacted(false), 700); + prevUsed.current = used; + return () => clearTimeout(t); + } + prevUsed.current = used; + }, [used]); + if (used === 0) return null; const pct = Math.min((used / limit) * 100, 100); const size = 20; @@ -24,6 +39,12 @@ export const ContextRing: React.FC<{ used: number; limit: number; accentColor: s strokeDasharray={circumference} strokeDashoffset={dashOffset} strokeLinecap="round" transform={`rotate(-90 ${size / 2} ${size / 2})`} + style={{ + // 600ms cubic-bezier ease on the fill: a rise glides up, a compaction + // glides down. The one-shot opacity dip is the "settle" flash on drop. + transition: 'stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.35s ease', + opacity: justCompacted ? 0.35 : 1, + }} /> diff --git a/frontend/src/app/pages/AgentChat/shell/ContextDrawer.tsx b/frontend/src/app/pages/AgentChat/shell/ContextDrawer.tsx index a6eeda15..1b49d684 100644 --- a/frontend/src/app/pages/AgentChat/shell/ContextDrawer.tsx +++ b/frontend/src/app/pages/AgentChat/shell/ContextDrawer.tsx @@ -53,7 +53,7 @@ export default function ContextDrawer() { {Math.round(ctxPct * 100)}% - {(session.tokens?.input || 0).toLocaleString()} / 200K tokens + {(session.tokens?.input || 0).toLocaleString()} / {session.context_window ? `${Math.round(session.context_window / 1000)}K` : '200K'} tokens diff --git a/frontend/src/shared/state/agentsSlice.ts b/frontend/src/shared/state/agentsSlice.ts index 5f104dcf..3bc0bb3e 100644 --- a/frontend/src/shared/state/agentsSlice.ts +++ b/frontend/src/shared/state/agentsSlice.ts @@ -744,13 +744,16 @@ const agentsSlice = createSlice({ const session = state.sessions[action.payload.sessionId]; if (!session) return; session.compacted_through_msg_id = action.payload.throughMsgId; - // The next preflight check looks at tokens.input to estimate "history used". - // After a compaction the real number is unknown until the next turn round-trips, - // but the OLD number is definitely wrong (it counts messages we just dropped). - // Zero it so preflight falls back to the char/4 estimate of remaining content, - // which is closer than holding the stale pre-compact value. + // After compaction the OLD context numbers are wrong (they count messages we + // just dropped). Reset EVERY context-derived field so all surfaces that show + // it — the toolbar ContextRing, the /context drawer %, the pre-send guard — + // reflect the post-compaction state, not just the token count. The real values + // refill on the next turn's round-trip; until then zero is closer than stale. if (action.payload.throughMsgId) { session.tokens = { input: 0, output: session.tokens?.output ?? 0 }; + session.ctx_used_pct = 0; + session.cache_read_pct = 0; + session.cache_read_tokens = 0; } },