mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[aidan] chat: auto-compact inside send, surface as chat activity (#63)
This commit is contained in:
@@ -201,6 +201,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
const [showScrollButton, setShowScrollButton] = useState(false);
|
||||
const [showResumeBubble, setShowResumeBubble] = useState(false);
|
||||
const [awaitingResponse, setAwaitingResponse] = useState(false);
|
||||
const [preSendActivityLabel, setPreSendActivityLabel] = useState<string | null>(null);
|
||||
const [activatingMcp, setActivatingMcp] = useState<string | null>(null);
|
||||
const [activateError, setActivateError] = useState<string | null>(null);
|
||||
const [mode, setMode] = useState('agent');
|
||||
@@ -385,6 +386,18 @@ const AgentChat: React.FC<AgentChatProps> = ({ 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<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{(awaitingResponse || (session.status === 'running' && !streamingMessageId)) && (
|
||||
{(preSendActivityLabel || awaitingResponse || (session.status === 'running' && !streamingMessageId)) && (
|
||||
<Box sx={{ overflowAnchor: 'none' }}>
|
||||
<ThinkingBubble
|
||||
label={session.turn_label?.label}
|
||||
label={preSendActivityLabel || session.turn_label?.label}
|
||||
seedKey={`${session.id}:${session.messages?.length ?? 0}`}
|
||||
/>
|
||||
</Box>
|
||||
@@ -1781,6 +1794,7 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
|
||||
autoFocus={autoFocus}
|
||||
thinkingLevel={session?.thinking_level ?? 'auto'}
|
||||
onThinkingLevelChange={handleThinkingLevelChange}
|
||||
onActivityLabelChange={setPreSendActivityLabel}
|
||||
/>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
|
||||
@@ -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<ChatInputHandle, Props>(({ onSend, disabled, mode, onModeChange, model, onModelChange, provider, onProviderChange, isRunning, onStop, autoRunMode, contextEstimate, embedded, autoFocus, sessionId, queueLength = 0, thinkingLevel = 'auto', onThinkingLevelChange }, ref) => {
|
||||
const ChatInput = forwardRef<ChatInputHandle, Props>(({ 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<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -73,6 +74,9 @@ const ChatInput = forwardRef<ChatInputHandle, Props>(({ 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<ChatInputHandle, Props>(({ 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<ChatInputHandle, Props>(({ 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<string, string> = { '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<string, string> = { '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<ChatInputHandle, Props>(({ 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,
|
||||
|
||||
@@ -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<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>
|
||||
);
|
||||
}
|
||||
if (sendBlock.kind !== 'too_long') return null;
|
||||
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}`,
|
||||
display: 'flex', alignItems: 'center', gap: 1,
|
||||
}}>
|
||||
<Box component="span" sx={{
|
||||
display: 'inline-block', width: 6, height: 6, borderRadius: '50%',
|
||||
bgcolor: c.accent.primary,
|
||||
animation: 'osw-pulse 1.2s ease-in-out infinite',
|
||||
'@keyframes osw-pulse': {
|
||||
'0%, 100%': { opacity: 0.4 },
|
||||
'50%': { opacity: 1 },
|
||||
},
|
||||
flexShrink: 0,
|
||||
}} />
|
||||
<Typography sx={{ fontSize: '0.88rem', color: c.text.primary, lineHeight: 1.45 }}>
|
||||
Making room for your message
|
||||
That message is too long to send. Try shortening it or splitting it into a few smaller ones.
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user